From b57b0bf63aa562cd4bf84ea128b4e78b5b0ee701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B2=BD=EC=A2=85?= Date: Fri, 12 Jun 2026 17:15:05 +0900 Subject: [PATCH] feat: start abaqus input parser --- docs/io-definitions/abaqus-input-parser-io.md | 139 ++++++++++++ phases/abaqus-input-parser/index.json | 65 ++++++ phases/abaqus-input-parser/step0.md | 65 ++++++ phases/abaqus-input-parser/step1.md | 83 +++++++ phases/abaqus-input-parser/step2.md | 63 ++++++ phases/abaqus-input-parser/step3.md | 62 +++++ phases/abaqus-input-parser/step4.md | 65 ++++++ phases/abaqus-input-parser/step5.md | 55 +++++ phases/index.json | 4 + scripts/test_validate_workspace.py | 58 +++-- scripts/validate_workspace.py | 27 ++- src/fesa/io/abaqus/input_parser.cpp | 214 ++++++++++++++++++ src/fesa/io/abaqus/input_parser.hpp | 20 ++ tests/unit/abaqus_input_parser_mesh_test.cpp | 80 +++++++ 14 files changed, 978 insertions(+), 22 deletions(-) create mode 100644 docs/io-definitions/abaqus-input-parser-io.md create mode 100644 phases/abaqus-input-parser/index.json create mode 100644 phases/abaqus-input-parser/step0.md create mode 100644 phases/abaqus-input-parser/step1.md create mode 100644 phases/abaqus-input-parser/step2.md create mode 100644 phases/abaqus-input-parser/step3.md create mode 100644 phases/abaqus-input-parser/step4.md create mode 100644 phases/abaqus-input-parser/step5.md create mode 100644 src/fesa/io/abaqus/input_parser.cpp create mode 100644 src/fesa/io/abaqus/input_parser.hpp create mode 100644 tests/unit/abaqus_input_parser_mesh_test.cpp diff --git a/docs/io-definitions/abaqus-input-parser-io.md b/docs/io-definitions/abaqus-input-parser-io.md new file mode 100644 index 0000000..9df0c0c --- /dev/null +++ b/docs/io-definitions/abaqus-input-parser-io.md @@ -0,0 +1,139 @@ +# Abaqus Input Parser I/O Definition + +## Metadata + +- feature_id: abaqus-input-parser +- status: ready-for-implementation-planning +- owner_agent: io-definition-agent +- date: 2026-06-12 + +## Abaqus Input Scope + +FESA supports only the Abaqus input keyword subset listed here. This document +does not claim full Abaqus compatibility. + +| keyword | support_status | level | required_parameters | mapped_internal_concept | unsupported-case behavior | +| --- | --- | --- | --- | --- | --- | +| `*HEADING` | supported | model | none | model title metadata | accepted; title storage may be deferred | +| `*NODE` | supported | model | none | node id and global coordinates | malformed rows are errors | +| `*ELEMENT` | supported | model | `TYPE` | element id, topology, connectivity | missing/unsupported `TYPE` is an error | +| `*NSET` | supported | model | `NSET` | named node set | missing name is an error | +| `*ELSET` | supported | model | `ELSET` | named element set | missing name is an error | +| `*MATERIAL` | supported | model | `NAME` | material identity | missing name is an error | +| `*ELASTIC` | supported | model | none | linear elastic constants | malformed rows are errors | +| section keyword | supported | model | `ELSET`, `MATERIAL` | property assignment | unsupported section type is an error | +| `*STEP` | supported | history | optional `NAME` | analysis step | malformed parameters are errors | +| `*STATIC` | supported | history | none | static procedure marker | unsupported procedures are errors | +| `*BOUNDARY` | supported | model/history | none | prescribed nodal dof values | malformed rows are errors | +| `*CLOAD` | supported | history | none | concentrated nodal load | malformed rows are errors | +| `*OUTPUT` | supported | history | none | output request root | unsupported options warn or error by context | +| `*NODE OUTPUT` | supported | history | none | nodal output request | unsupported quantities warn or error by context | +| `*ELEMENT OUTPUT` | supported | history | none | element output request | unsupported quantities warn or error by context | + +Initial V0 element names are two-node line/bar types only: `T2D2`, `T3D2`, +`C3D2`, and `B31`. + +## Syntax Policy + +- Keywords and parameter names are case-insensitive. +- Keyword lines begin with `*`. +- Comment lines begin with `**` and are ignored. +- Blank lines are ignored. +- Fields are comma-separated. Leading and trailing whitespace around fields is + ignored. +- Empty required fields are parse errors. +- Unsupported keywords are errors unless this contract explicitly marks them as + ignored-with-warning. +- Diagnostics must include severity, a stable code, a human-readable message, + and enough line context to locate the input row. +- Include files are out of scope for this parser phase. + +## Model Data Mapping + +- Nodes map an Abaqus node label to a FESA node id and three global coordinate + components. Missing trailing coordinate components are interpreted as `0.0` + only when the keyword-specific test documents that behavior. +- Elements map an Abaqus element label, supported element type, and connectivity + labels to a FESA element. Section assignment supplies material/property + linkage in a later parser slice. +- Node sets and element sets preserve deterministic membership order. +- Materials map by Abaqus `NAME`. Linear elastic data maps to the semantic + material contract used by solver implementation. +- Section keywords bind an element set to a material and create the semantic + property assignment needed by the solver. +- The global coordinate system is assumed. Units are user-consistent and are not + converted by the parser. + +## History Data Mapping + +- `*STEP` begins an ordered analysis step. If no name is provided, the parser + assigns a deterministic step name or id. +- `*STATIC` marks the step as a linear static procedure for V0. +- `*BOUNDARY` maps node id, dof range, and value to prescribed boundary + conditions. +- `*CLOAD` maps node id, dof component, and magnitude to concentrated loads. +- Output request keywords define requested quantities when semantic storage + exists; unsupported quantities must not be silently accepted. + +## Internal Model Contract + +- `Domain` owns model definition data created from the input file. +- Parsed model objects should be treated as immutable after parsing where + practical. +- `AnalysisStep` owns step-local boundary conditions and loads. +- The parser must not store equation ids on nodes or elements. +- Parser diagnostics are part of the result; callers do not need to inspect + partial `Domain` state to detect failure. + +## Output HDF5 Schema + +The parser itself does not write HDF5. Solver output remains authoritative in +`results.h5` and follows the project HDF5 contract: + +| quantity | dataset_path | location | component policy | +| --- | --- | --- | --- | +| displacement | `/steps//frames//field_outputs/U` | nodal | `U1`, `U2`, `U3` as applicable | +| reaction | `/steps//frames//field_outputs/RF` | nodal | `RF1`, `RF2`, `RF3` as applicable | +| internal force | `/steps//frames//field_outputs/element_forces` | element | feature-specific | +| stress | `/steps//frames//field_outputs/S` | integration point or element | feature-specific | + +Required metadata includes schema version, model id, source input identity, +coordinate system, units policy, solver version, step/frame identity, and row +identity fields. + +## FESA HDF5 To Reference CSV Comparison Schema + +Reference comparison reads `results.h5` and matches deterministic rows against +Abaqus-generated CSV files under `reference//`. This parser phase does +not generate or modify those reference artifacts. + +Common row identity: + +- sort order: step, frame, entity id, location, component +- node id and element id are Abaqus labels preserved by the parser +- component names follow the HDF5 component policy + +CSV files remain: + +- `_displacements.csv` +- `_reactions.csv` +- `_internalforces.csv` +- `_stresses.csv` + +## Validation Rules + +- Duplicate node, element, material, property, set, or step labels are errors. +- Missing references are errors. +- Unsupported keywords are errors unless explicitly documented otherwise. +- Malformed numeric fields are errors. +- Parser unit tests must cover valid subset mapping and invalid subset + diagnostics before production parser changes. +- Workspace validation remains `python scripts/validate_workspace.py`. + +## Downstream Handoff + +Implementation planning should split work into mesh keyword parsing, +diagnostics, set/section mapping, material/history mapping, and integration +validation. Reference model work may later use this subset to prepare +`reference//model.inp`, but this phase must not create or modify +Abaqus reference CSV artifacts. diff --git a/phases/abaqus-input-parser/index.json b/phases/abaqus-input-parser/index.json new file mode 100644 index 0000000..5d4de34 --- /dev/null +++ b/phases/abaqus-input-parser/index.json @@ -0,0 +1,65 @@ +{ + "project": "FESA Structural Solver", + "phase": "abaqus-input-parser", + "steps": [ + { + "step": 0, + "name": "io-contract", + "status": "completed", + "allowed_paths": [ + "docs/io-definitions/abaqus-input-parser-io.md" + ], + "summary": "Abaqus input parser I/O contract added" + }, + { + "step": 1, + "name": "mesh-keyword-parser", + "status": "completed", + "allowed_paths": [ + "src/fesa/io/abaqus/", + "tests/unit/abaqus_input_parser_*_test.cpp" + ], + "summary": "Mesh keyword parser maps NODE and ELEMENT data into Domain" + }, + { + "step": 2, + "name": "syntax-diagnostics", + "status": "pending", + "allowed_paths": [ + "src/fesa/io/abaqus/", + "tests/unit/abaqus_input_parser_*_test.cpp" + ] + }, + { + "step": 3, + "name": "sets-and-section-properties", + "status": "pending", + "allowed_paths": [ + "src/fesa/io/abaqus/", + "src/fesa/model/", + "tests/unit/abaqus_input_parser_*_test.cpp", + "tests/unit/model_*_test.cpp" + ] + }, + { + "step": 4, + "name": "material-step-load-parser", + "status": "pending", + "allowed_paths": [ + "src/fesa/io/abaqus/", + "src/fesa/model/", + "tests/unit/abaqus_input_parser_*_test.cpp", + "tests/unit/model_*_test.cpp" + ] + }, + { + "step": 5, + "name": "integration-validation-report", + "status": "pending", + "allowed_paths": [ + "tests/integration/", + "docs/build-test-reports/abaqus-input-parser.md" + ] + } + ] +} diff --git a/phases/abaqus-input-parser/step0.md b/phases/abaqus-input-parser/step0.md new file mode 100644 index 0000000..13b68f6 --- /dev/null +++ b/phases/abaqus-input-parser/step0.md @@ -0,0 +1,65 @@ +# Step 0: io-contract + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PRD.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- `/docs/SOLVER_AGENT_DESIGN.md` +- `/docs/io-definitions/README.md` + +## Task + +Create the feature-level I/O contract for the Abaqus input parser at +`/docs/io-definitions/abaqus-input-parser-io.md`. + +The contract must explicitly state that FESA supports only the documented +Abaqus keyword subset, not full Abaqus compatibility. Keep this document at a +semantic level. Do not design C++ APIs and do not implement parser code in this +step. + +Required scope: + +- Model data keywords: `*HEADING`, `*NODE`, `*ELEMENT`, `*NSET`, `*ELSET`, + `*MATERIAL`, `*ELASTIC`, and section keywords needed for V0 bar/truss use. +- History data keywords: `*STEP`, `*STATIC`, `*BOUNDARY`, `*CLOAD`, + `*OUTPUT`, `*NODE OUTPUT`, and `*ELEMENT OUTPUT`. +- Syntax policy for comments beginning with `**`, case-insensitive keywords, + comma-separated parameters and data, blank lines, unsupported keywords, and + parse diagnostics. +- Internal semantic mapping to `Domain`, nodes, elements, sets, materials, + properties, `AnalysisStep`, boundary conditions, loads, and output requests. +- HDF5 and reference CSV comparison schema summary consistent with existing + PRD/architecture documents. Do not create or modify reference artifacts. + +## Tests To Write First + +No C++ tests are required in this documentation-only step. + +## Acceptance Criteria + +```powershell +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +## Verification Procedure + +1. Run the acceptance criteria commands. +2. Confirm the contract does not claim full Abaqus compatibility. +3. Confirm every supported keyword has purpose, data requirements, mapping, and + unsupported-case behavior. +4. Update `phases/abaqus-input-parser/index.json` step 0: + - success: `"status": "completed"`, `"summary": "Abaqus input parser I/O contract added"` + - error after three attempts: `"status": "error"`, `"error_message": ""` + - blocked: `"status": "blocked"`, `"blocked_reason": ""` + +## Forbidden + +- Do not implement parser code. +- Do not design C++ APIs. +- Do not run Abaqus, Nastran, or any reference solver. +- Do not generate or modify Abaqus reference CSV files. diff --git a/phases/abaqus-input-parser/step1.md b/phases/abaqus-input-parser/step1.md new file mode 100644 index 0000000..ae27c03 --- /dev/null +++ b/phases/abaqus-input-parser/step1.md @@ -0,0 +1,83 @@ +# Step 1: mesh-keyword-parser + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PRD.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- `/docs/io-definitions/abaqus-input-parser-io.md` +- `/src/fesa/model/domain.hpp` +- `/src/fesa/model/node.hpp` +- `/src/fesa/model/element.hpp` +- `/tests/unit/model_domain_test.cpp` + +Review step 0 output before implementing. Keep this step limited to parser API +and mesh keyword mapping. + +## Task + +Add the first C++ parser slice under `/src/fesa/io/abaqus/`. + +Required behavior: + +- Provide a small `InputParser` that accepts an in-memory `.inp` string and + returns a parse result containing `fesa::model::Domain` and + `fesa::core::Status`. +- Parse `*HEADING` as accepted but not yet stored. +- Parse `*NODE` data lines into `Domain::add_node`. +- Parse `*ELEMENT, TYPE=` data lines into `Domain::add_element`. +- Support only two-node line/bar element names needed by the current model: + `T2D2`, `T3D2`, `B31`, and `C3D2`. +- Map `T2D2`, `T3D2`, and `C3D2` to `ElementTopology::truss2`; map `B31` to + `ElementTopology::bar2`. +- Use `PropertyId{0}` for elements in this first mesh-only slice because + section parsing is introduced later. +- Treat keywords and parameter names case-insensitively. +- Skip blank lines and comment lines that begin with `**`. + +Keep implementation C++17/MSVC-compatible. Do not introduce external +libraries, regex-heavy parser frameworks, JavaScript, TypeScript, or npm +fallbacks. + +## Tests To Write First + +Add `/tests/unit/abaqus_input_parser_mesh_test.cpp` before production code. + +The test must first fail because `fesa/io/abaqus/input_parser.hpp` does not +exist, then pass after implementation. It should verify: + +- a small input with `*HEADING`, `*NODE`, and `*ELEMENT, TYPE=T3D2` returns + `Status::is_ok() == true`; +- the returned `Domain` contains the expected node ids and 3D coordinates; +- the returned `Domain` contains the expected element id, topology, + connectivity, and `PropertyId{0}`; +- keyword and parameter casing are accepted case-insensitively. + +## Acceptance Criteria + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R abaqus_input_parser_mesh_test +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +## Verification Procedure + +1. Run the targeted CTest after writing the test and before production code; + confirm the failure is due to the missing parser API. +2. Implement the minimum code needed for the test to pass. +3. Run all acceptance criteria commands. +4. Confirm C++ production changes have a related C++ test file. +5. Update `phases/abaqus-input-parser/index.json` step 1: + - success: `"status": "completed"`, `"summary": "Mesh keyword parser maps NODE and ELEMENT data into Domain"` + - error after three attempts: `"status": "error"`, `"error_message": ""` + - blocked: `"status": "blocked"`, `"blocked_reason": ""` + +## Forbidden + +- Do not add set, material, section, load, boundary condition, output request, + include-file, or HDF5 behavior in this step. +- Do not mutate reference artifacts. diff --git a/phases/abaqus-input-parser/step2.md b/phases/abaqus-input-parser/step2.md new file mode 100644 index 0000000..ff0e321 --- /dev/null +++ b/phases/abaqus-input-parser/step2.md @@ -0,0 +1,63 @@ +# Step 2: syntax-diagnostics + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- `/docs/io-definitions/abaqus-input-parser-io.md` +- `/src/fesa/io/abaqus/input_parser.hpp` +- `/src/fesa/io/abaqus/input_parser.cpp` +- `/tests/unit/abaqus_input_parser_mesh_test.cpp` + +Review completed step summaries in `/phases/abaqus-input-parser/index.json`. + +## Task + +Add structured diagnostics for parser syntax and unsupported subset behavior. + +Required behavior: + +- Unsupported keywords produce `Status::is_ok() == false` with an error + diagnostic code and message that identifies the keyword. +- Malformed `*NODE` and `*ELEMENT` data lines produce error diagnostics. +- Missing required `TYPE` on `*ELEMENT` produces an error diagnostic. +- Unsupported element types produce an error diagnostic and do not add that + element. +- Diagnostics must include enough context in the message to locate the line + number. +- Existing valid mesh parser behavior remains unchanged. + +## Tests To Write First + +Extend `/tests/unit/abaqus_input_parser_mesh_test.cpp` or add +`/tests/unit/abaqus_input_parser_diagnostics_test.cpp` before production code. + +Test: + +- unsupported keyword failure; +- malformed node row failure; +- missing element `TYPE` failure; +- unsupported element type failure. + +Run the targeted CTest and confirm the new tests fail for missing behavior +before implementation. + +## Acceptance Criteria + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R abaqus_input_parser +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +## Verification Procedure + +Update step 2 status with summary or a concrete error/blocked reason. + +## Forbidden + +- Do not add new semantic model fields except what diagnostics require. +- Do not add material, section, step, boundary, or load parsing. diff --git a/phases/abaqus-input-parser/step3.md b/phases/abaqus-input-parser/step3.md new file mode 100644 index 0000000..2b14276 --- /dev/null +++ b/phases/abaqus-input-parser/step3.md @@ -0,0 +1,62 @@ +# Step 3: sets-and-section-properties + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- `/docs/io-definitions/abaqus-input-parser-io.md` +- `/src/fesa/model/domain.hpp` +- `/src/fesa/model/element.hpp` +- `/src/fesa/model/property.hpp` +- `/src/fesa/io/abaqus/input_parser.hpp` +- `/src/fesa/io/abaqus/input_parser.cpp` + +Review completed parser tests and phase summaries first. + +## Task + +Extend the model and parser only as needed to represent set membership and +section-to-property assignment for V0 bar/truss models. + +Required behavior: + +- Parse `*NSET, NSET=` node membership rows. +- Parse `*ELSET, ELSET=` element membership rows. +- Parse one V0 section keyword that assigns a material name to an element set. + Prefer the smallest section subset compatible with the current V0 element + path. +- Preserve deterministic member ordering as read, unless the I/O contract + explicitly states otherwise. +- Keep `Domain` as the owner of model definition data. Do not store equation ids + on nodes or elements. + +## Tests To Write First + +Add focused C++ tests before production code: + +- a model test for any new set/section semantic model API; +- a parser test proving `*NSET`, `*ELSET`, and the selected section keyword map + into the semantic model. + +Run targeted CTest and confirm the tests fail before implementation. + +## Acceptance Criteria + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "(model_|abaqus_input_parser_)" +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +## Verification Procedure + +Update step 3 status with summary or a concrete error/blocked reason. + +## Forbidden + +- Do not parse materials beyond names needed for section linkage. +- Do not add loads, boundary conditions, or analysis steps. +- Do not generate or modify reference artifacts. diff --git a/phases/abaqus-input-parser/step4.md b/phases/abaqus-input-parser/step4.md new file mode 100644 index 0000000..e45288d --- /dev/null +++ b/phases/abaqus-input-parser/step4.md @@ -0,0 +1,65 @@ +# Step 4: material-step-load-parser + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PRD.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- `/docs/io-definitions/abaqus-input-parser-io.md` +- `/src/fesa/model/material.hpp` +- `/src/fesa/model/analysis_step.hpp` +- `/src/fesa/model/boundary_condition.hpp` +- `/src/fesa/model/load.hpp` +- `/src/fesa/io/abaqus/input_parser.hpp` +- `/src/fesa/io/abaqus/input_parser.cpp` + +Review completed parser and model tests first. + +## Task + +Extend the parser for the V0 material and history data subset. + +Required behavior: + +- Parse `*MATERIAL, NAME=` and `*ELASTIC` data into the internal material + representation. If the current `Material` model cannot store elastic data, + add the smallest semantic extension with tests. +- Parse `*STEP` and `*STATIC` into ordered `AnalysisStep` definitions. +- Parse `*BOUNDARY` rows into step boundary conditions using existing + `DofComponent` mapping. +- Parse `*CLOAD` rows into step concentrated loads using existing + `DofComponent` mapping. +- Parse `*OUTPUT`, `*NODE OUTPUT`, and `*ELEMENT OUTPUT` only to the extent + documented in the I/O contract. If semantic storage is not yet present, record + an explicit ignored-with-warning diagnostic rather than inventing output model + APIs. + +## Tests To Write First + +Add focused C++ tests before production code: + +- model tests for any new material or output request semantic fields; +- parser tests for material/elastic, step/static, boundary, and cload rows. + +Run targeted CTest and confirm the tests fail before implementation. + +## Acceptance Criteria + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "(model_|abaqus_input_parser_)" +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +## Verification Procedure + +Update step 4 status with summary or a concrete error/blocked reason. + +## Forbidden + +- Do not implement analysis execution, assembly, HDF5 writing, or reference + comparison in this step. +- Do not mutate reference artifacts. diff --git a/phases/abaqus-input-parser/step5.md b/phases/abaqus-input-parser/step5.md new file mode 100644 index 0000000..73a60e4 --- /dev/null +++ b/phases/abaqus-input-parser/step5.md @@ -0,0 +1,55 @@ +# Step 5: integration-validation-report + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PRD.md` +- `/docs/ARCHITECTURE.md` +- `/docs/ADR.md` +- `/docs/io-definitions/abaqus-input-parser-io.md` +- `/src/fesa/io/abaqus/input_parser.hpp` +- `/tests/unit/abaqus_input_parser_mesh_test.cpp` +- existing parser/model tests added by prior steps + +Review all completed step summaries in `/phases/abaqus-input-parser/index.json`. + +## Task + +Add an integration-level parser test and a build/test report. + +Required behavior: + +- Add a small integration test that parses a V0 Abaqus `.inp` string containing + the supported parser subset and validates the resulting `Domain` and + `AnalysisStep` data. +- Keep the test in memory; do not create or modify Abaqus reference CSV + artifacts. +- Write `/docs/build-test-reports/abaqus-input-parser.md` with command evidence, + exit codes, and known limitations. + +## Tests To Write First + +Add `/tests/integration/abaqus_input_parser_integration_test.cpp` before any +final integration adjustments. Confirm it fails for the missing integrated +behavior, then make it pass with the minimum implementation-owned changes. + +## Acceptance Criteria + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R abaqus_input_parser +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +## Verification Procedure + +1. Run the acceptance criteria commands. +2. Confirm no reference artifacts were generated or modified. +3. Update step 5 status with summary or a concrete error/blocked reason. + +## Forbidden + +- Do not claim reference comparison, physics sanity, or release readiness. +- Do not generate, restore, or modify Abaqus reference CSV files. diff --git a/phases/index.json b/phases/index.json index ac29268..b05d72b 100644 --- a/phases/index.json +++ b/phases/index.json @@ -3,6 +3,10 @@ { "dir": "solver-core-skeleton", "status": "completed" + }, + { + "dir": "abaqus-input-parser", + "status": "pending" } ] } diff --git a/scripts/test_validate_workspace.py b/scripts/test_validate_workspace.py index 71ba75a..5966551 100644 --- a/scripts/test_validate_workspace.py +++ b/scripts/test_validate_workspace.py @@ -30,14 +30,15 @@ class ValidateWorkspaceTests(unittest.TestCase): (root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8") build_dir = root / "build" / "msvc-debug" with patch.dict(os.environ, {}, clear=True): - self.assertEqual( - validate_workspace.discover_commands(root), - [ - f'cmake -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64', - f'cmake --build "{build_dir}" --config Debug', - f'ctest --test-dir "{build_dir}" --output-on-failure -C Debug', - ], - ) + with patch.object(validate_workspace.shutil, "which", return_value="C:\\tools\\cmake.exe"): + self.assertEqual( + validate_workspace.discover_commands(root), + [ + f'cmake -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64', + f'cmake --build "{build_dir}" --config Debug', + f'ctest --test-dir "{build_dir}" --output-on-failure -C Debug', + ], + ) def test_msvc_debug_configure_preset_is_preferred_when_present(self): validate_workspace = load_validate_workspace() @@ -60,14 +61,39 @@ class ValidateWorkspaceTests(unittest.TestCase): encoding="utf-8", ) with patch.dict(os.environ, {}, clear=True): - self.assertEqual( - validate_workspace.discover_commands(root), - [ - "cmake --preset msvc-debug", - f'cmake --build "{root / "out" / "msvc-debug"}" --config Debug', - f'ctest --test-dir "{root / "out" / "msvc-debug"}" --output-on-failure -C Debug', - ], - ) + with patch.object(validate_workspace.shutil, "which", return_value="C:\\tools\\cmake.exe"): + self.assertEqual( + validate_workspace.discover_commands(root), + [ + "cmake --preset msvc-debug", + f'cmake --build "{root / "out" / "msvc-debug"}" --config Debug', + f'ctest --test-dir "{root / "out" / "msvc-debug"}" --output-on-failure -C Debug', + ], + ) + + def test_cmake_commands_use_common_install_path_when_tools_are_not_on_path(self): + validate_workspace = load_validate_workspace() + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) / "project" + root.mkdir() + (root / "CMakeLists.txt").write_text("cmake_minimum_required(VERSION 3.20)\n", encoding="utf-8") + common_bin = Path(tmp) / "CMake" / "bin" + common_bin.mkdir(parents=True) + (common_bin / "cmake.exe").write_text("", encoding="utf-8") + (common_bin / "ctest.exe").write_text("", encoding="utf-8") + build_dir = root / "build" / "msvc-debug" + + with patch.dict(os.environ, {}, clear=True): + with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin): + with patch.object(validate_workspace.shutil, "which", return_value=None): + self.assertEqual( + validate_workspace.discover_commands(root), + [ + f'"{common_bin / "cmake.exe"}" -S "{root}" -B "{build_dir}" -G "Visual Studio 17 2022" -A x64', + f'"{common_bin / "cmake.exe"}" --build "{build_dir}" --config Debug', + f'"{common_bin / "ctest.exe"}" --test-dir "{build_dir}" --output-on-failure -C Debug', + ], + ) def test_no_cmake_project_has_no_validation_commands(self): validate_workspace = load_validate_workspace() diff --git a/scripts/validate_workspace.py b/scripts/validate_workspace.py index 45b4530..7154891 100644 --- a/scripts/validate_workspace.py +++ b/scripts/validate_workspace.py @@ -32,6 +32,17 @@ def _cmake_config() -> tuple[str, str, str, Path]: return generator, platform, config, build_dir +def _tool_command(tool_name: str) -> str: + if shutil.which(tool_name) is not None: + return tool_name + + exe = COMMON_CMAKE_BIN / f"{tool_name}.exe" + if exe.exists(): + return f'"{exe}"' + + return tool_name + + def _read_presets(root: Path) -> dict: presets_file = root / "CMakePresets.json" if not presets_file.exists(): @@ -53,13 +64,15 @@ def _preset_binary_dir(root: Path, preset: dict) -> Path: def load_preset_commands(root: Path) -> list[str]: payload = _read_presets(root) config = os.environ.get("HARNESS_CMAKE_CONFIG", DEFAULT_CONFIG) + cmake = _tool_command("cmake") + ctest = _tool_command("ctest") for preset in payload.get("configurePresets", []): if isinstance(preset, dict) and preset.get("name") == PRESET_NAME: build_dir = _preset_binary_dir(root, preset) return [ - f"cmake --preset {PRESET_NAME}", - f'cmake --build "{build_dir}" --config {config}', - f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}', + f"{cmake} --preset {PRESET_NAME}", + f'{cmake} --build "{build_dir}" --config {config}', + f'{ctest} --test-dir "{build_dir}" --output-on-failure -C {config}', ] return [] @@ -71,10 +84,12 @@ def load_cmake_commands(root: Path) -> list[str]: generator, platform, config, build_dir = _cmake_config() if not build_dir.is_absolute(): build_dir = root / build_dir + cmake = _tool_command("cmake") + ctest = _tool_command("ctest") return [ - f'cmake -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}', - f'cmake --build "{build_dir}" --config {config}', - f'ctest --test-dir "{build_dir}" --output-on-failure -C {config}', + f'{cmake} -S "{root}" -B "{build_dir}" -G "{generator}" -A {platform}', + f'{cmake} --build "{build_dir}" --config {config}', + f'{ctest} --test-dir "{build_dir}" --output-on-failure -C {config}', ] diff --git a/src/fesa/io/abaqus/input_parser.cpp b/src/fesa/io/abaqus/input_parser.cpp new file mode 100644 index 0000000..b54b003 --- /dev/null +++ b/src/fesa/io/abaqus/input_parser.cpp @@ -0,0 +1,214 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace fesa::io::abaqus { +namespace { + +enum class Section { + none, + heading, + node, + element +}; + +struct ElementSection { + model::ElementTopology topology = model::ElementTopology::unknown; +}; + +std::string trim(std::string_view text) +{ + auto first = text.begin(); + auto last = text.end(); + while (first != last && std::isspace(static_cast(*first)) != 0) { + ++first; + } + while (first != last && std::isspace(static_cast(*(last - 1))) != 0) { + --last; + } + return std::string(first, last); +} + +std::string lower(std::string text) +{ + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return text; +} + +std::vector split_commas(std::string_view line) +{ + std::vector fields; + std::size_t start = 0; + while (start <= line.size()) { + const auto comma = line.find(',', start); + const auto end = comma == std::string_view::npos ? line.size() : comma; + fields.push_back(trim(line.substr(start, end - start))); + if (comma == std::string_view::npos) { + break; + } + start = comma + 1; + } + return fields; +} + +bool parse_int(const std::string& text, int& value) +{ + try { + std::size_t parsed = 0; + value = std::stoi(text, &parsed); + return parsed == text.size(); + } catch (const std::invalid_argument&) { + return false; + } catch (const std::out_of_range&) { + return false; + } +} + +bool parse_double(const std::string& text, double& value) +{ + try { + std::size_t parsed = 0; + value = std::stod(text, &parsed); + return parsed == text.size(); + } catch (const std::invalid_argument&) { + return false; + } catch (const std::out_of_range&) { + return false; + } +} + +model::ElementTopology topology_for_type(const std::string& raw_type) +{ + const auto type = lower(raw_type); + if (type == "t2d2" || type == "t3d2" || type == "c3d2") { + return model::ElementTopology::truss2; + } + if (type == "b31") { + return model::ElementTopology::bar2; + } + return model::ElementTopology::unknown; +} + +std::string parameter_value(const std::vector& fields, const std::string& key) +{ + const auto expected_key = lower(key); + for (std::size_t i = 1; i < fields.size(); ++i) { + const auto equals = fields[i].find('='); + if (equals == std::string::npos) { + continue; + } + const auto field_key = lower(trim(std::string_view{fields[i]}.substr(0, equals))); + if (field_key == expected_key) { + return trim(std::string_view{fields[i]}.substr(equals + 1)); + } + } + return {}; +} + +void parse_node_line(model::Domain& domain, const std::vector& fields) +{ + if (fields.size() != 4) { + return; + } + + int id = 0; + std::array coordinates{}; + if (!parse_int(fields[0], id) || + !parse_double(fields[1], coordinates[0]) || + !parse_double(fields[2], coordinates[1]) || + !parse_double(fields[3], coordinates[2])) { + return; + } + + domain.add_node(model::Node{core::NodeId{id}, coordinates}); +} + +void parse_element_line(model::Domain& domain, + const ElementSection& element_section, + const std::vector& fields) +{ + if (element_section.topology == model::ElementTopology::unknown) { + return; + } + if (fields.size() != 3) { + return; + } + + int id = 0; + int first_node = 0; + int second_node = 0; + if (!parse_int(fields[0], id) || !parse_int(fields[1], first_node) || !parse_int(fields[2], second_node)) { + return; + } + + domain.add_element(model::Element{ + core::ElementId{id}, + element_section.topology, + {core::NodeId{first_node}, core::NodeId{second_node}}, + core::PropertyId{0} + }); +} + +} // namespace + +ParseResult InputParser::parse(std::string_view input) const +{ + ParseResult result{model::Domain{}, core::Status::ok()}; + Section section = Section::none; + ElementSection element_section{}; + std::size_t start = 0; + + while (start <= input.size()) { + const auto newline = input.find('\n', start); + const auto end = newline == std::string_view::npos ? input.size() : newline; + auto line = input.substr(start, end - start); + if (!line.empty() && line.back() == '\r') { + line.remove_suffix(1); + } + + const auto cleaned = trim(line); + if (cleaned.empty() || cleaned.rfind("**", 0) == 0) { + if (newline == std::string_view::npos) { + break; + } + start = newline + 1; + continue; + } + + if (cleaned.front() == '*') { + const auto keyword_fields = split_commas(std::string_view{cleaned}.substr(1)); + const auto keyword = keyword_fields.empty() ? std::string{} : lower(keyword_fields[0]); + if (keyword == "heading") { + section = Section::heading; + } else if (keyword == "node") { + section = Section::node; + } else if (keyword == "element") { + section = Section::element; + const auto type = parameter_value(keyword_fields, "type"); + element_section.topology = topology_for_type(type); + } else { + section = Section::none; + } + } else if (section == Section::node) { + parse_node_line(result.domain, split_commas(cleaned)); + } else if (section == Section::element) { + parse_element_line(result.domain, element_section, split_commas(cleaned)); + } + + if (newline == std::string_view::npos) { + break; + } + start = newline + 1; + } + + return result; +} + +} // namespace fesa::io::abaqus diff --git a/src/fesa/io/abaqus/input_parser.hpp b/src/fesa/io/abaqus/input_parser.hpp new file mode 100644 index 0000000..b25a8f4 --- /dev/null +++ b/src/fesa/io/abaqus/input_parser.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#include + +namespace fesa::io::abaqus { + +struct ParseResult { + model::Domain domain; + core::Status status; +}; + +class InputParser { +public: + ParseResult parse(std::string_view input) const; +}; + +} // namespace fesa::io::abaqus diff --git a/tests/unit/abaqus_input_parser_mesh_test.cpp b/tests/unit/abaqus_input_parser_mesh_test.cpp new file mode 100644 index 0000000..1803e05 --- /dev/null +++ b/tests/unit/abaqus_input_parser_mesh_test.cpp @@ -0,0 +1,80 @@ +#include + +#include + +namespace { + +int fail() +{ + return 1; +} + +} // namespace + +int main() +{ + fesa::io::abaqus::InputParser parser; + + const std::string input = R"inp( +** mesh parser smoke case +*HeAdInG +small truss model +*NoDe +1, 0.0, 0.0, 0.0 +2, 1.5, 0.0, 0.0 +*ElEmEnT, TyPe=t3d2 +10, 1, 2 +)inp"; + + const auto result = parser.parse(input); + if (!result.status.is_ok()) { + return fail(); + } + + const auto& domain = result.domain; + if (domain.nodes().size() != 2 || domain.elements().size() != 1) { + return fail(); + } + + const auto* first_node = domain.find_node(fesa::core::NodeId{1}); + if (first_node == nullptr || first_node->coordinates()[0] != 0.0 || + first_node->coordinates()[1] != 0.0 || first_node->coordinates()[2] != 0.0) { + return fail(); + } + + const auto* second_node = domain.find_node(fesa::core::NodeId{2}); + if (second_node == nullptr || second_node->coordinates()[0] != 1.5 || + second_node->coordinates()[1] != 0.0 || second_node->coordinates()[2] != 0.0) { + return fail(); + } + + const auto* element = domain.find_element(fesa::core::ElementId{10}); + if (element == nullptr || + element->topology() != fesa::model::ElementTopology::truss2 || + element->node_ids().size() != 2 || + element->node_ids()[0].value != 1 || + element->node_ids()[1].value != 2 || + element->property_id().value != 0) { + return fail(); + } + + const std::string beam_input = R"inp( +*NODE +1, 0.0, 0.0, 0.0 +2, 0.0, 2.0, 0.0 +*ELEMENT, TYPE=B31 +20, 1, 2 +)inp"; + + const auto beam_result = parser.parse(beam_input); + const auto* beam = beam_result.domain.find_element(fesa::core::ElementId{20}); + if (!beam_result.status.is_ok() || + beam == nullptr || + beam->topology() != fesa::model::ElementTopology::bar2 || + beam->node_ids().size() != 2 || + beam->property_id().value != 0) { + return fail(); + } + + return 0; +}