From 2b83b1128f9884653ced1faaf272afe7c17688c3 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sat, 1 Aug 2026 02:24:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(abaqus-subset-completion):=20step=200=20?= =?UTF-8?q?=E2=80=94=20abaqus-input-contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ABAQUS_INPUT_SUBSET.md | 298 ++++++++++++++++++ phases/index.json | 2 +- tests/CMakeLists.txt | 9 + tests/fixtures/abaqus/contract.tsv | 54 ++++ .../abaqus/invalid/assembly_multiple.inp | 4 + .../invalid/assembly_set_wrong_instance.inp | 8 + .../abaqus/invalid/boundary_conflict.inp | 11 + .../abaqus/invalid/cload_invalid_dof.inp | 7 + .../abaqus/invalid/elastic_invalid_data.inp | 6 + .../abaqus/invalid/element_wrong_type.inp | 8 + .../abaqus/invalid/elset_invalid_generate.inp | 5 + .../abaqus/invalid/end_step_missing.inp | 2 + .../invalid/heading_invalid_parameter.inp | 1 + .../abaqus/invalid/instance_local_mesh.inp | 8 + .../abaqus/invalid/instance_transform.inp | 7 + .../invalid/material_missing_elastic.inp | 4 + tests/fixtures/abaqus/invalid/mixed_mesh.inp | 8 + .../abaqus/invalid/nested_set_cycle.inp | 7 + .../abaqus/invalid/node_duplicate.inp | 6 + .../abaqus/invalid/nset_missing_name.inp | 5 + .../invalid/output_invalid_parameter.inp | 4 + .../fixtures/abaqus/invalid/part_unclosed.inp | 3 + .../invalid/preprint_invalid_parameter.inp | 1 + .../abaqus/invalid/restart_outside_step.inp | 1 + .../invalid/section_duplicate_assignment.inp | 12 + .../abaqus/invalid/section_wrong_type.inp | 9 + .../abaqus/invalid/static_invalid_data.inp | 4 + .../fixtures/abaqus/invalid/step_multiple.inp | 6 + .../abaqus/invalid/transverse_nonzero_scf.inp | 11 + .../abaqus/invalid/unknown_include.inp | 1 + .../valid/explicit_transverse_shear.inp | 24 ++ tests/fixtures/abaqus/valid/flat_complete.inp | 22 ++ .../abaqus/valid/generated_nested_sets.inp | 28 ++ .../abaqus/valid/hierarchical_complete.inp | 31 ++ .../fixtures/abaqus/valid/noop_directives.inp | 28 ++ tests/unit/io/abaqus/input_contract_test.cpp | 294 +++++++++++++++++ 36 files changed, 938 insertions(+), 1 deletion(-) create mode 100644 docs/ABAQUS_INPUT_SUBSET.md create mode 100644 tests/fixtures/abaqus/contract.tsv create mode 100644 tests/fixtures/abaqus/invalid/assembly_multiple.inp create mode 100644 tests/fixtures/abaqus/invalid/assembly_set_wrong_instance.inp create mode 100644 tests/fixtures/abaqus/invalid/boundary_conflict.inp create mode 100644 tests/fixtures/abaqus/invalid/cload_invalid_dof.inp create mode 100644 tests/fixtures/abaqus/invalid/elastic_invalid_data.inp create mode 100644 tests/fixtures/abaqus/invalid/element_wrong_type.inp create mode 100644 tests/fixtures/abaqus/invalid/elset_invalid_generate.inp create mode 100644 tests/fixtures/abaqus/invalid/end_step_missing.inp create mode 100644 tests/fixtures/abaqus/invalid/heading_invalid_parameter.inp create mode 100644 tests/fixtures/abaqus/invalid/instance_local_mesh.inp create mode 100644 tests/fixtures/abaqus/invalid/instance_transform.inp create mode 100644 tests/fixtures/abaqus/invalid/material_missing_elastic.inp create mode 100644 tests/fixtures/abaqus/invalid/mixed_mesh.inp create mode 100644 tests/fixtures/abaqus/invalid/nested_set_cycle.inp create mode 100644 tests/fixtures/abaqus/invalid/node_duplicate.inp create mode 100644 tests/fixtures/abaqus/invalid/nset_missing_name.inp create mode 100644 tests/fixtures/abaqus/invalid/output_invalid_parameter.inp create mode 100644 tests/fixtures/abaqus/invalid/part_unclosed.inp create mode 100644 tests/fixtures/abaqus/invalid/preprint_invalid_parameter.inp create mode 100644 tests/fixtures/abaqus/invalid/restart_outside_step.inp create mode 100644 tests/fixtures/abaqus/invalid/section_duplicate_assignment.inp create mode 100644 tests/fixtures/abaqus/invalid/section_wrong_type.inp create mode 100644 tests/fixtures/abaqus/invalid/static_invalid_data.inp create mode 100644 tests/fixtures/abaqus/invalid/step_multiple.inp create mode 100644 tests/fixtures/abaqus/invalid/transverse_nonzero_scf.inp create mode 100644 tests/fixtures/abaqus/invalid/unknown_include.inp create mode 100644 tests/fixtures/abaqus/valid/explicit_transverse_shear.inp create mode 100644 tests/fixtures/abaqus/valid/flat_complete.inp create mode 100644 tests/fixtures/abaqus/valid/generated_nested_sets.inp create mode 100644 tests/fixtures/abaqus/valid/hierarchical_complete.inp create mode 100644 tests/fixtures/abaqus/valid/noop_directives.inp create mode 100644 tests/unit/io/abaqus/input_contract_test.cpp diff --git a/docs/ABAQUS_INPUT_SUBSET.md b/docs/ABAQUS_INPUT_SUBSET.md new file mode 100644 index 0000000..4b26bb0 --- /dev/null +++ b/docs/ABAQUS_INPUT_SUBSET.md @@ -0,0 +1,298 @@ +# FESA Phase 1 Abaqus Input Subset + +## 1. Status and scope + +This document is the normative input contract for the FESA Phase 1 Abaqus +adapter. FESA accepts only the keywords, parameters, scopes, and data forms +defined here. It does not implement general Abaqus input syntax, and it never +silently ignores an unknown keyword or option. + +Two mutually exclusive organizations are supported: + +- a flat/orphan mesh whose mesh and set records are in global scope; or +- one or more `*PART` definitions followed by exactly one `*ASSEMBLY` that + contains exactly one untransformed `*INSTANCE` of the active Part. + +Only the active Part is normalized into `Domain`. Names and external labels are +input identity; generated nonnegative FESA IDs and dense indices are separate. +FESA performs no unit conversion. + +`tests/fixtures/abaqus/contract.tsv` is the executable valid/invalid fixture +matrix for this document. Its expected diagnostic code and line are part of +the contract. A diagnostic for a bad data row points to that row; a keyword, +parameter, or scope error points to the keyword row. + +## 2. Common lexical rules + +- Input is UTF-8. A UTF-8 BOM is accepted only at the start of the file. +- Blank lines and lines whose first non-whitespace characters are `**` are + ignored. A comment or blank line does not end the current keyword record. +- Keyword names, parameter names, flag parameters, and the enumerated values + named below are ASCII case-insensitive. Entity names are trimmed and then + matched exactly. +- Fields are comma-separated and surrounding ASCII whitespace is ignored. +- External node and element labels are positive signed 64-bit integers. DOF + numbers are integers 1 through 6. Real fields must be finite. +- A flag parameter has no `=` value. A valued parameter must have one nonempty + value. Duplicate parameters are `abaqus.syntax.duplicate_parameter`. +- Parameters not listed for a keyword are + `abaqus.syntax.unsupported_parameter`. Unknown keywords, including + `*INCLUDE`, are `abaqus.unsupported_keyword`. +- A non-comment data line without an open data-bearing keyword is + `abaqus.syntax.data_without_keyword`. + +## 3. Scope and ordering + +The parser maintains `global`, `part`, `assembly`, `instance`, and `step` +scope. `*STEP` is a global child scope: model-data records already opened in +global scope remain model data, while `*STATIC`, `*CLOAD`, `*RESTART`, and +`*OUTPUT` belong to the open Step. + +The accepted ordering is: + +```text +optional *HEADING and *PREPRINT +flat mesh, or one or more *PART blocks and one *ASSEMBLY block +global materials +optional global model-data *BOUNDARY +one *STEP + one *STATIC + optional *BOUNDARY and *CLOAD + optional no-op *RESTART and *OUTPUT +*END STEP +``` + +References are resolved after the complete deck is parsed. A material may +therefore follow the Part that uses it, and a nested set may refer to a set +declared later in the same scope. + +## 4. Mesh and hierarchy keywords + +### 4.1 `*NODE` + +- Scope: flat global or Part; not Assembly, Instance, or Step. +- Parameters: none. +- Data: one or more `label, x, y, z` rows, with exactly four nonempty fields. +- Semantics: labels are unique in their mesh scope. Reuse in an inactive Part + is allowed because it is a different Part scope. +- Diagnostics: `abaqus.syntax.invalid_node_scope`, + `abaqus.semantic.invalid_node_data`, `abaqus.semantic.duplicate_node_label`. + +### 4.2 `*ELEMENT` + +- Scope: flat global or Part. +- Parameters: required `TYPE=B31`; optional `ELSET=`; no others. +- Data: one or more `element_label, node_1_label, node_2_label` rows. +- Semantics: element labels are unique in their mesh scope. Both nodes must + exist in that scope. `ELSET=` adds every row to the named element set and + merges with an explicit set of the same name using sorted-unique membership. +- Diagnostics: `abaqus.syntax.invalid_element_scope`, + `abaqus.semantic.unsupported_element`, + `abaqus.semantic.invalid_element_data`, + `abaqus.semantic.duplicate_element_label`, + `abaqus.semantic.missing_node`. + +### 4.3 Part delimiters + +`*PART` is global-only, requires exactly `NAME=`, and accepts no data. +Part names are unique. `*END PART` accepts no parameters or data and closes the +open Part. Nesting or a mismatched delimiter is invalid. + +Diagnostics are `abaqus.syntax.invalid_part_scope`, +`abaqus.syntax.unexpected_end_part`, `abaqus.syntax.unclosed_part`, and +`abaqus.semantic.duplicate_part`. + +### 4.4 Assembly delimiters + +`*ASSEMBLY` is global-only, requires exactly `NAME=`, and accepts no +data. Phase 1 accepts exactly one Assembly. `*END ASSEMBLY` has no parameters +or data and closes the open Assembly. + +Diagnostics are `abaqus.syntax.invalid_assembly_scope`, +`abaqus.syntax.multiple_assemblies`, +`abaqus.syntax.unexpected_end_assembly`, and +`abaqus.syntax.unclosed_assembly`. + +### 4.5 Instance delimiters + +`*INSTANCE` is Assembly-only and requires exactly `NAME=, PART=`. +Phase 1 accepts exactly one Instance. No translation or rotation data and no +keyword record are allowed inside it. `*END INSTANCE` has no parameters or +data. + +Diagnostics are `abaqus.syntax.invalid_instance_scope`, +`abaqus.syntax.instance_local_keyword`, +`abaqus.syntax.unexpected_end_instance`, +`abaqus.syntax.unclosed_instance`, `abaqus.semantic.instance_count`, +`abaqus.semantic.instance_transform`, and `abaqus.semantic.missing_part`. +A transform diagnostic points to the first transform data row. + +Flat `*NODE`/`*ELEMENT` records combined with any Part/Assembly organization +are `abaqus.semantic.mixed_mesh_organization`. + +## 5. Sets + +`*NSET` and `*ELSET` are allowed in flat global, Part, or Assembly scope. + +- Required parameter: respectively `NSET=` or `ELSET=`. +- Optional flag: `GENERATE`. +- Assembly scope additionally requires `INSTANCE=`. + `INSTANCE=` is forbidden in flat global and Part scope. +- Explicit data consists of comma-separated positive labels and/or names of + sets of the same kind and scope. Empty trailing fields are ignored. +- `GENERATE` data consists of exactly one `start, end, increment` row. All + values are positive integers, `start <= end`, and `(end-start)` is divisible + by `increment`. +- Repeated declarations of the same set merge. Nested references are resolved + independent of declaration order, cycles are rejected, and final membership + is deterministic sorted-unique. +- An Assembly set lifts active-Part local labels through its named Instance. + It cannot reference an inactive or unknown Instance. + +Diagnostics are `abaqus.syntax.invalid_set_scope`, +`abaqus.semantic.invalid_generate`, `abaqus.semantic.set_cycle`, +`abaqus.semantic.missing_set_member`, and +`abaqus.semantic.wrong_instance`. + +## 6. Material and Beam section + +### 6.1 `*MATERIAL` and `*ELASTIC` + +`*MATERIAL` is global-only, requires exactly `NAME=`, and has no data. +Material names are unique. Its `*ELASTIC` child is global model data, has no +parameters, and has exactly one `young_modulus, poisson_ratio` row. Young's +modulus is finite and positive; Poisson's ratio is finite and satisfies +`-1 < nu < 0.5`. Temperature and field dependencies are not supported. + +Diagnostics are `abaqus.syntax.invalid_material_scope`, +`abaqus.semantic.duplicate_material`, +`abaqus.semantic.elastic_without_material`, +`abaqus.semantic.missing_elastic`, and +`abaqus.semantic.invalid_elastic_data`. + +### 6.2 `*BEAM GENERAL SECTION` + +- Scope: flat global or Part. +- Parameters: required `SECTION=GENERAL`, `ELSET=`, and + `MATERIAL=`; no others. +- First data row: exactly `A, I_y, I_yz, I_z, J`. `A`, `I_y`, `I_z`, and `J` + are finite and positive; `I_yz` must be finite and exactly zero for Phase 1. +- Second data row: exactly three finite components of the local section-axis + reference direction. Model validation rejects a zero direction or one + parallel to an assigned element axis. +- The material and set may be declared later, but must resolve. Every active + B31 element has exactly one section assignment. + +Diagnostics are `abaqus.syntax.invalid_section_scope`, +`abaqus.semantic.unsupported_section`, +`abaqus.semantic.invalid_section_data`, +`abaqus.semantic.missing_material`, +`abaqus.semantic.missing_element_set`, +`abaqus.semantic.missing_section`, and +`abaqus.semantic.duplicate_section_assignment`. + +### 6.3 `*TRANSVERSE SHEAR STIFFNESS` + +This optional record is in the same flat-global or Part scope as, and must +immediately follow, the affected `*BEAM GENERAL SECTION`. It has no parameters +and exactly one `K23, K13, SCF` data row. `K23` and `K13` are finite and +positive. Phase 1 accepts only numeric `SCF=0`; omitted/default `0.25`, nonzero +values, and the Abaqus `SCF` label are unsupported. + +For isotropic `G=E/[2(1+nu)]`, FESA stores +`A_sy=K23/G` and `A_sz=K13/G` with source `input`. If this keyword is absent, +the semantic mapper stores `A_sy=A_sz=5A/6`, `SCF=0`, with source +`phase1_default`. + +The data order follows the Abaqus 2024 +[*TRANSVERSE SHEAR STIFFNESS* reference](https://docs.software.vt.edu/abaqusv2024/English/SIMACAEKEYRefMap/simakey-r-transverseshearstiffness.htm); +the restriction to numeric zero SCF and the effective-area mapping are FESA +Phase 1 decisions. + +Diagnostics are `abaqus.syntax.invalid_transverse_shear_scope`, +`abaqus.semantic.orphan_transverse_shear`, +`abaqus.semantic.invalid_transverse_shear_data`, and +`abaqus.semantic.nonzero_scf`. + +## 7. Linear static step, BC, and load + +### 7.1 `*BOUNDARY` + +`*BOUNDARY` is allowed as global model data before the Step or inside the sole +Step. It has no parameters. Each row is +`node-or-nset, first_dof[, last_dof[, value]]`. `last_dof` defaults to +`first_dof`; value defaults to zero. The inclusive DOF range is 1 through 6. +Repeated identical prescriptions are deduplicated; differing values for one +node/DOF are rejected. + +Diagnostics are `abaqus.syntax.invalid_boundary_scope`, +`abaqus.semantic.invalid_boundary_data`, +`abaqus.semantic.invalid_dof`, `abaqus.semantic.invalid_dof_range`, +`abaqus.semantic.missing_node_target`, and +`abaqus.semantic.conflicting_boundary`. + +### 7.2 `*CLOAD` + +`*CLOAD` is Step-only and has no parameters. Each row is exactly +`node-or-nset, dof, magnitude`; DOF is 1 through 6 and magnitude is finite. +Loads on the same node/DOF are summed in input order after target resolution. + +Diagnostics are `abaqus.syntax.invalid_cload_scope`, +`abaqus.semantic.invalid_cload_data`, `abaqus.semantic.invalid_dof`, and +`abaqus.semantic.missing_node_target`. + +### 7.3 `*STEP`, `*STATIC`, and `*END STEP` + +`*STEP` is global-only. Optional parameters are `NAME=` and +`NLGEOM=NO`; omitted name becomes `Step-1`. Exactly one Step is required. +`NLGEOM=YES` and every other option are unsupported. + +Exactly one `*STATIC` occurs inside the Step. It has no parameters and accepts +either no data row or one row of one through four finite positive values +`initial_increment[, time_period[, minimum_increment[, maximum_increment]]]`. +The values are accepted as load-step metadata; Phase 1 performs one linear +solve. + +`*END STEP` has no parameters or data and closes the Step. + +Diagnostics are `abaqus.syntax.invalid_step_scope`, +`abaqus.syntax.unexpected_end_step`, `abaqus.syntax.unclosed_step`, +`abaqus.semantic.step_count`, `abaqus.semantic.unsupported_step_option`, +`abaqus.semantic.missing_static`, and +`abaqus.semantic.invalid_static_data`. + +## 8. Recognized no-op directives + +These records are deliberately recognized and do not create Domain entities: + +- `*HEADING`: global-only, no parameters, zero or more text data rows. +- `*PREPRINT`: global-only, optional `ECHO`, `MODEL`, `HISTORY`, and `CONTACT` + parameters, each with value `YES` or `NO`; no data. +- `*RESTART`: Step-only, optional `WRITE` flag and optional nonnegative integer + `FREQUENCY`; no data. +- `*OUTPUT`: Step-only, exactly one `FIELD` or `HISTORY` flag and optional + `VARIABLE=PRESELECT`; no data. + +Diagnostics are the common unsupported-parameter diagnostic plus +`abaqus.syntax.invalid_heading_scope`, +`abaqus.syntax.invalid_preprint_scope`, +`abaqus.syntax.invalid_restart_scope`, and +`abaqus.syntax.invalid_output_scope`. There is no general no-op or +ignore-unknown path. + +## 9. Fixture matrix contract + +The tab-separated manifest columns are: + +```text +case_id, outcome, fixture, expected_stage, expected_code, expected_line, +node_count, element_count, node_set_count, element_set_count, +prescribed_dof_count, nodal_load_count, shear_source, shear_area_y, +shear_area_z, checked_node_set, checked_element_set +``` + +For a valid case, the public `parse_deck()` and `map_deck_to_domain()` path must +produce a Domain matching every populated expected field. Set checks use +`name:local-label,...`. For an invalid case, that same public path must fail +and contain the exact stage, code, and source line in the manifest. Partial +decks and test-only semantic construction are not accepted. diff --git a/phases/index.json b/phases/index.json index a209a82..2a6e147 100644 --- a/phases/index.json +++ b/phases/index.json @@ -46,4 +46,4 @@ "status": "pending" } ] -} \ No newline at end of file +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c4b2ad3..f52859e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -118,6 +118,7 @@ add_test( ) add_executable(fesa_abaqus_parser_tests + unit/io/abaqus/input_contract_test.cpp unit/io/abaqus/parser_test.cpp ) @@ -147,6 +148,14 @@ add_test( --gtest_filter=ScopedDeck.* ) +add_test( + NAME AbaqusInputContract + COMMAND "$" + --gtest_filter=AbaqusInputContract/* +) +# Steps 1-4 make this normative matrix pass, then remove WILL_FAIL. +set_tests_properties(AbaqusInputContract PROPERTIES WILL_FAIL TRUE) + add_executable(fesa_deck_to_domain_tests integration/io/minimal_deck_to_domain_test.cpp ) diff --git a/tests/fixtures/abaqus/contract.tsv b/tests/fixtures/abaqus/contract.tsv new file mode 100644 index 0000000..5c33aff --- /dev/null +++ b/tests/fixtures/abaqus/contract.tsv @@ -0,0 +1,54 @@ +# case_id outcome fixture expected_stage expected_code expected_line node_count element_count node_set_count element_set_count prescribed_dof_count nodal_load_count shear_source shear_area_y shear_area_z checked_node_set checked_element_set +node_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +element_b31_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +nset_explicit_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +elset_explicit_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +material_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +elastic_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +beam_general_section_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +boundary_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +cload_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +step_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +static_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +end_step_valid valid valid/flat_complete.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +part_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +end_part_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +assembly_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +end_assembly_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +instance_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +end_instance_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +assembly_nset_instance_valid valid valid/hierarchical_complete.inp - - 0 2 1 2 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +generate_nset_valid valid valid/generated_nested_sets.inp - - 0 3 2 2 2 18 1 phase1_default 0.8333333333333334 0.8333333333333334 AllNodes:1,2,3 AllElements:1,2 +generate_elset_valid valid valid/generated_nested_sets.inp - - 0 3 2 2 2 18 1 phase1_default 0.8333333333333334 0.8333333333333334 AllNodes:1,2,3 AllElements:1,2 +nested_sets_valid valid valid/generated_nested_sets.inp - - 0 3 2 2 2 18 1 phase1_default 0.8333333333333334 0.8333333333333334 AllNodes:1,2,3 AllElements:1,2 +transverse_shear_valid valid valid/explicit_transverse_shear.inp - - 0 2 1 1 1 6 1 input 0.8 0.5 Fixed:1 Beam:1 +heading_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +preprint_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +restart_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +output_noop_valid valid valid/noop_directives.inp - - 0 2 1 1 1 6 1 phase1_default 0.8333333333333334 0.8333333333333334 Fixed:1 Beam:1 +node_duplicate_invalid invalid invalid/node_duplicate.inp semantic abaqus.semantic.duplicate_node_label 3 - - - - - - - - - - - +element_type_invalid invalid invalid/element_wrong_type.inp semantic abaqus.semantic.unsupported_element 4 - - - - - - - - - - - +part_unclosed_invalid invalid invalid/part_unclosed.inp syntax abaqus.syntax.unclosed_part 1 - - - - - - - - - - - +assembly_multiple_invalid invalid invalid/assembly_multiple.inp syntax abaqus.syntax.multiple_assemblies 3 - - - - - - - - - - - +instance_transform_invalid invalid invalid/instance_transform.inp semantic abaqus.semantic.instance_transform 5 - - - - - - - - - - - +instance_local_mesh_invalid invalid invalid/instance_local_mesh.inp syntax abaqus.syntax.instance_local_keyword 5 - - - - - - - - - - - +mixed_mesh_invalid invalid invalid/mixed_mesh.inp semantic abaqus.semantic.mixed_mesh_organization 1 - - - - - - - - - - - +nset_parameter_invalid invalid invalid/nset_missing_name.inp semantic abaqus.semantic.missing_parameter 1 - - - - - - - - - - - +elset_generate_invalid invalid invalid/elset_invalid_generate.inp semantic abaqus.semantic.invalid_generate 2 - - - - - - - - - - - +nested_set_cycle_invalid invalid invalid/nested_set_cycle.inp semantic abaqus.semantic.set_cycle 4 - - - - - - - - - - - +set_instance_invalid invalid invalid/assembly_set_wrong_instance.inp semantic abaqus.semantic.wrong_instance 6 - - - - - - - - - - - +material_missing_elastic_invalid invalid invalid/material_missing_elastic.inp semantic abaqus.semantic.missing_elastic 1 - - - - - - - - - - - +elastic_data_invalid invalid invalid/elastic_invalid_data.inp semantic abaqus.semantic.invalid_elastic_data 3 - - - - - - - - - - - +section_type_invalid invalid invalid/section_wrong_type.inp semantic abaqus.semantic.unsupported_section 4 - - - - - - - - - - - +section_duplicate_invalid invalid invalid/section_duplicate_assignment.inp semantic abaqus.semantic.duplicate_section_assignment 7 - - - - - - - - - - - +transverse_scf_invalid invalid invalid/transverse_nonzero_scf.inp semantic abaqus.semantic.nonzero_scf 8 - - - - - - - - - - - +boundary_conflict_invalid invalid invalid/boundary_conflict.inp semantic abaqus.semantic.conflicting_boundary 10 - - - - - - - - - - - +cload_dof_invalid invalid invalid/cload_invalid_dof.inp semantic abaqus.semantic.invalid_dof 6 - - - - - - - - - - - +step_multiple_invalid invalid invalid/step_multiple.inp semantic abaqus.semantic.step_count 4 - - - - - - - - - - - +static_data_invalid invalid invalid/static_invalid_data.inp semantic abaqus.semantic.invalid_static_data 3 - - - - - - - - - - - +end_step_missing_invalid invalid invalid/end_step_missing.inp syntax abaqus.syntax.unclosed_step 1 - - - - - - - - - - - +heading_parameter_invalid invalid invalid/heading_invalid_parameter.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - - +preprint_parameter_invalid invalid invalid/preprint_invalid_parameter.inp syntax abaqus.syntax.unsupported_parameter 1 - - - - - - - - - - - +restart_scope_invalid invalid invalid/restart_outside_step.inp syntax abaqus.syntax.invalid_restart_scope 1 - - - - - - - - - - - +output_parameter_invalid invalid invalid/output_invalid_parameter.inp syntax abaqus.syntax.unsupported_parameter 3 - - - - - - - - - - - +unknown_keyword_invalid invalid invalid/unknown_include.inp syntax abaqus.unsupported_keyword 1 - - - - - - - - - - - diff --git a/tests/fixtures/abaqus/invalid/assembly_multiple.inp b/tests/fixtures/abaqus/invalid/assembly_multiple.inp new file mode 100644 index 0000000..551c0aa --- /dev/null +++ b/tests/fixtures/abaqus/invalid/assembly_multiple.inp @@ -0,0 +1,4 @@ +*ASSEMBLY, NAME=First +*END ASSEMBLY +*ASSEMBLY, NAME=Second +*END ASSEMBLY diff --git a/tests/fixtures/abaqus/invalid/assembly_set_wrong_instance.inp b/tests/fixtures/abaqus/invalid/assembly_set_wrong_instance.inp new file mode 100644 index 0000000..b3867e2 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/assembly_set_wrong_instance.inp @@ -0,0 +1,8 @@ +*PART, NAME=BeamPart +*END PART +*ASSEMBLY, NAME=Root +*INSTANCE, NAME=Beam-1, PART=BeamPart +*END INSTANCE +*NSET, NSET=Fixed, INSTANCE=Other +1 +*END ASSEMBLY diff --git a/tests/fixtures/abaqus/invalid/boundary_conflict.inp b/tests/fixtures/abaqus/invalid/boundary_conflict.inp new file mode 100644 index 0000000..14ac18d --- /dev/null +++ b/tests/fixtures/abaqus/invalid/boundary_conflict.inp @@ -0,0 +1,11 @@ +*NODE +1, 0.0, 0.0, 0.0 +*NSET, NSET=Fixed +1 +*STEP +*STATIC +*BOUNDARY +Fixed, 1, 1, 0.0 +*BOUNDARY +Fixed, 1, 1, 1.0 +*END STEP diff --git a/tests/fixtures/abaqus/invalid/cload_invalid_dof.inp b/tests/fixtures/abaqus/invalid/cload_invalid_dof.inp new file mode 100644 index 0000000..3667214 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/cload_invalid_dof.inp @@ -0,0 +1,7 @@ +*NODE +1, 0.0, 0.0, 0.0 +*STEP +*STATIC +*CLOAD +1, 7, 1.0 +*END STEP diff --git a/tests/fixtures/abaqus/invalid/elastic_invalid_data.inp b/tests/fixtures/abaqus/invalid/elastic_invalid_data.inp new file mode 100644 index 0000000..55e113b --- /dev/null +++ b/tests/fixtures/abaqus/invalid/elastic_invalid_data.inp @@ -0,0 +1,6 @@ +*MATERIAL, NAME=Steel +*ELASTIC +-1.0, 0.25 +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/element_wrong_type.inp b/tests/fixtures/abaqus/invalid/element_wrong_type.inp new file mode 100644 index 0000000..291af82 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/element_wrong_type.inp @@ -0,0 +1,8 @@ +*NODE +1, 0.0, 0.0, 0.0 +2, 1.0, 0.0, 0.0 +*ELEMENT, TYPE=B32, ELSET=Beam +1, 1, 2 +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/elset_invalid_generate.inp b/tests/fixtures/abaqus/invalid/elset_invalid_generate.inp new file mode 100644 index 0000000..62d8281 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/elset_invalid_generate.inp @@ -0,0 +1,5 @@ +*ELSET, ELSET=Beam, GENERATE +3, 1, 1 +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/end_step_missing.inp b/tests/fixtures/abaqus/invalid/end_step_missing.inp new file mode 100644 index 0000000..587ba36 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/end_step_missing.inp @@ -0,0 +1,2 @@ +*STEP +*STATIC diff --git a/tests/fixtures/abaqus/invalid/heading_invalid_parameter.inp b/tests/fixtures/abaqus/invalid/heading_invalid_parameter.inp new file mode 100644 index 0000000..db40b96 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/heading_invalid_parameter.inp @@ -0,0 +1 @@ +*HEADING, NAME=Unsupported diff --git a/tests/fixtures/abaqus/invalid/instance_local_mesh.inp b/tests/fixtures/abaqus/invalid/instance_local_mesh.inp new file mode 100644 index 0000000..3d8a08d --- /dev/null +++ b/tests/fixtures/abaqus/invalid/instance_local_mesh.inp @@ -0,0 +1,8 @@ +*PART, NAME=BeamPart +*END PART +*ASSEMBLY, NAME=Root +*INSTANCE, NAME=Beam-1, PART=BeamPart +*NODE +1, 0.0, 0.0, 0.0 +*END INSTANCE +*END ASSEMBLY diff --git a/tests/fixtures/abaqus/invalid/instance_transform.inp b/tests/fixtures/abaqus/invalid/instance_transform.inp new file mode 100644 index 0000000..0f6e1de --- /dev/null +++ b/tests/fixtures/abaqus/invalid/instance_transform.inp @@ -0,0 +1,7 @@ +*PART, NAME=BeamPart +*END PART +*ASSEMBLY, NAME=Root +*INSTANCE, NAME=Beam-1, PART=BeamPart +1.0, 2.0, 3.0 +*END INSTANCE +*END ASSEMBLY diff --git a/tests/fixtures/abaqus/invalid/material_missing_elastic.inp b/tests/fixtures/abaqus/invalid/material_missing_elastic.inp new file mode 100644 index 0000000..10c2893 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/material_missing_elastic.inp @@ -0,0 +1,4 @@ +*MATERIAL, NAME=Steel +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/mixed_mesh.inp b/tests/fixtures/abaqus/invalid/mixed_mesh.inp new file mode 100644 index 0000000..ae060b0 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/mixed_mesh.inp @@ -0,0 +1,8 @@ +*NODE +1, 0.0, 0.0, 0.0 +*PART, NAME=BeamPart +*END PART +*ASSEMBLY, NAME=Root +*INSTANCE, NAME=Beam-1, PART=BeamPart +*END INSTANCE +*END ASSEMBLY diff --git a/tests/fixtures/abaqus/invalid/nested_set_cycle.inp b/tests/fixtures/abaqus/invalid/nested_set_cycle.inp new file mode 100644 index 0000000..5314e78 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/nested_set_cycle.inp @@ -0,0 +1,7 @@ +*NSET, NSET=First +Second +*NSET, NSET=Second +First +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/node_duplicate.inp b/tests/fixtures/abaqus/invalid/node_duplicate.inp new file mode 100644 index 0000000..2e7e5bc --- /dev/null +++ b/tests/fixtures/abaqus/invalid/node_duplicate.inp @@ -0,0 +1,6 @@ +*NODE +1, 0.0, 0.0, 0.0 +1, 1.0, 0.0, 0.0 +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/nset_missing_name.inp b/tests/fixtures/abaqus/invalid/nset_missing_name.inp new file mode 100644 index 0000000..5a4cf9e --- /dev/null +++ b/tests/fixtures/abaqus/invalid/nset_missing_name.inp @@ -0,0 +1,5 @@ +*NSET +1 +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/output_invalid_parameter.inp b/tests/fixtures/abaqus/invalid/output_invalid_parameter.inp new file mode 100644 index 0000000..b1e3dc5 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/output_invalid_parameter.inp @@ -0,0 +1,4 @@ +*STEP +*STATIC +*OUTPUT, FIELD, VARIABLE=ALL +*END STEP diff --git a/tests/fixtures/abaqus/invalid/part_unclosed.inp b/tests/fixtures/abaqus/invalid/part_unclosed.inp new file mode 100644 index 0000000..5ea3947 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/part_unclosed.inp @@ -0,0 +1,3 @@ +*PART, NAME=BeamPart +*NODE +1, 0.0, 0.0, 0.0 diff --git a/tests/fixtures/abaqus/invalid/preprint_invalid_parameter.inp b/tests/fixtures/abaqus/invalid/preprint_invalid_parameter.inp new file mode 100644 index 0000000..5b88143 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/preprint_invalid_parameter.inp @@ -0,0 +1 @@ +*PREPRINT, COLOR=YES diff --git a/tests/fixtures/abaqus/invalid/restart_outside_step.inp b/tests/fixtures/abaqus/invalid/restart_outside_step.inp new file mode 100644 index 0000000..1cc500a --- /dev/null +++ b/tests/fixtures/abaqus/invalid/restart_outside_step.inp @@ -0,0 +1 @@ +*RESTART, WRITE, FREQUENCY=0 diff --git a/tests/fixtures/abaqus/invalid/section_duplicate_assignment.inp b/tests/fixtures/abaqus/invalid/section_duplicate_assignment.inp new file mode 100644 index 0000000..ee325f1 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/section_duplicate_assignment.inp @@ -0,0 +1,12 @@ +*MATERIAL, NAME=Steel +*ELASTIC +200.0, 0.25 +*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel +1.0, 1.0, 0.0, 1.0, 1.0 +0.0, 1.0, 0.0 +*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel +1.0, 2.0, 0.0, 2.0, 1.0 +0.0, 1.0, 0.0 +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/section_wrong_type.inp b/tests/fixtures/abaqus/invalid/section_wrong_type.inp new file mode 100644 index 0000000..e83b08e --- /dev/null +++ b/tests/fixtures/abaqus/invalid/section_wrong_type.inp @@ -0,0 +1,9 @@ +*MATERIAL, NAME=Steel +*ELASTIC +200.0, 0.25 +*BEAM GENERAL SECTION, SECTION=ARBITRARY, ELSET=Beam, MATERIAL=Steel +1.0, 1.0, 0.0, 1.0, 1.0 +0.0, 1.0, 0.0 +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/static_invalid_data.inp b/tests/fixtures/abaqus/invalid/static_invalid_data.inp new file mode 100644 index 0000000..7e6d712 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/static_invalid_data.inp @@ -0,0 +1,4 @@ +*STEP +*STATIC +1.0, -1.0 +*END STEP diff --git a/tests/fixtures/abaqus/invalid/step_multiple.inp b/tests/fixtures/abaqus/invalid/step_multiple.inp new file mode 100644 index 0000000..6c4608a --- /dev/null +++ b/tests/fixtures/abaqus/invalid/step_multiple.inp @@ -0,0 +1,6 @@ +*STEP, NAME=First +*STATIC +*END STEP +*STEP, NAME=Second +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/transverse_nonzero_scf.inp b/tests/fixtures/abaqus/invalid/transverse_nonzero_scf.inp new file mode 100644 index 0000000..a1fb9a2 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/transverse_nonzero_scf.inp @@ -0,0 +1,11 @@ +*MATERIAL, NAME=Steel +*ELASTIC +200.0, 0.25 +*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel +1.0, 1.0, 0.0, 1.0, 1.0 +0.0, 1.0, 0.0 +*TRANSVERSE SHEAR STIFFNESS +64.0, 40.0, 0.25 +*STEP +*STATIC +*END STEP diff --git a/tests/fixtures/abaqus/invalid/unknown_include.inp b/tests/fixtures/abaqus/invalid/unknown_include.inp new file mode 100644 index 0000000..d1af3e3 --- /dev/null +++ b/tests/fixtures/abaqus/invalid/unknown_include.inp @@ -0,0 +1 @@ +*INCLUDE, INPUT=other.inp diff --git a/tests/fixtures/abaqus/valid/explicit_transverse_shear.inp b/tests/fixtures/abaqus/valid/explicit_transverse_shear.inp new file mode 100644 index 0000000..7b48363 --- /dev/null +++ b/tests/fixtures/abaqus/valid/explicit_transverse_shear.inp @@ -0,0 +1,24 @@ +*NODE +1, 0.0, 0.0, 0.0 +2, 1.0, 0.0, 0.0 +*ELEMENT, TYPE=B31, ELSET=Beam +1, 1, 2 +*NSET, NSET=Fixed +1 +*ELSET, ELSET=Beam +1 +*MATERIAL, NAME=Steel +*ELASTIC +200.0, 0.25 +*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel +1.0, 1.0, 0.0, 1.0, 1.0 +0.0, 1.0, 0.0 +*TRANSVERSE SHEAR STIFFNESS +64.0, 40.0, 0.0 +*STEP +*STATIC +*BOUNDARY +Fixed, 1, 6 +*CLOAD +2, 2, -1.0 +*END STEP diff --git a/tests/fixtures/abaqus/valid/flat_complete.inp b/tests/fixtures/abaqus/valid/flat_complete.inp new file mode 100644 index 0000000..5ad71e9 --- /dev/null +++ b/tests/fixtures/abaqus/valid/flat_complete.inp @@ -0,0 +1,22 @@ +*NODE +1, 0.0, 0.0, 0.0 +2, 1.0, 0.0, 0.0 +*ELEMENT, TYPE=B31, ELSET=Beam +1, 1, 2 +*NSET, NSET=Fixed +1 +*ELSET, ELSET=Beam +1 +*MATERIAL, NAME=Steel +*ELASTIC +200.0, 0.25 +*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel +1.0, 1.0, 0.0, 1.0, 1.0 +0.0, 1.0, 0.0 +*BOUNDARY +Fixed, 1, 6 +*STEP, NAME=Load +*STATIC +*CLOAD +2, 2, -1.0 +*END STEP diff --git a/tests/fixtures/abaqus/valid/generated_nested_sets.inp b/tests/fixtures/abaqus/valid/generated_nested_sets.inp new file mode 100644 index 0000000..34ef9fe --- /dev/null +++ b/tests/fixtures/abaqus/valid/generated_nested_sets.inp @@ -0,0 +1,28 @@ +*NODE +1, 0.0, 0.0, 0.0 +2, 1.0, 0.0, 0.0 +3, 2.0, 0.0, 0.0 +*ELEMENT, TYPE=B31 +1, 1, 2 +2, 2, 3 +*NSET, NSET=FirstTwo, GENERATE +1, 2, 1 +*NSET, NSET=AllNodes +FirstTwo, 3 +*ELSET, ELSET=GeneratedElements, GENERATE +1, 2, 1 +*ELSET, ELSET=AllElements +GeneratedElements +*MATERIAL, NAME=Steel +*ELASTIC +200.0, 0.25 +*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=AllElements, MATERIAL=Steel +1.0, 1.0, 0.0, 1.0, 1.0 +0.0, 1.0, 0.0 +*STEP +*STATIC +*BOUNDARY +AllNodes, 1, 6 +*CLOAD +3, 2, -1.0 +*END STEP diff --git a/tests/fixtures/abaqus/valid/hierarchical_complete.inp b/tests/fixtures/abaqus/valid/hierarchical_complete.inp new file mode 100644 index 0000000..89b8c7c --- /dev/null +++ b/tests/fixtures/abaqus/valid/hierarchical_complete.inp @@ -0,0 +1,31 @@ +*PART, NAME=BeamPart +*NODE +1, 0.0, 0.0, 0.0 +2, 1.0, 0.0, 0.0 +*ELEMENT, TYPE=B31, ELSET=Beam +1, 1, 2 +*ELSET, ELSET=Beam +1 +*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel +1.0, 1.0, 0.0, 1.0, 1.0 +0.0, 1.0, 0.0 +*END PART +*ASSEMBLY, NAME=RootAssembly +*INSTANCE, NAME=Beam-1, PART=BeamPart +*END INSTANCE +*NSET, NSET=Fixed, INSTANCE=Beam-1 +1 +*NSET, NSET=Tip, INSTANCE=Beam-1 +2 +*END ASSEMBLY +*MATERIAL, NAME=Steel +*ELASTIC +200.0, 0.25 +*STEP, NAME=Load, NLGEOM=NO +*STATIC +1.0, 1.0, 0.01, 1.0 +*BOUNDARY +Fixed, 1, 6 +*CLOAD +Tip, 2, -1.0 +*END STEP diff --git a/tests/fixtures/abaqus/valid/noop_directives.inp b/tests/fixtures/abaqus/valid/noop_directives.inp new file mode 100644 index 0000000..76cf7d9 --- /dev/null +++ b/tests/fixtures/abaqus/valid/noop_directives.inp @@ -0,0 +1,28 @@ +*HEADING +FESA recognized no-op heading +*PREPRINT, ECHO=NO, MODEL=NO, HISTORY=NO, CONTACT=NO +*NODE +1, 0.0, 0.0, 0.0 +2, 1.0, 0.0, 0.0 +*ELEMENT, TYPE=B31, ELSET=Beam +1, 1, 2 +*NSET, NSET=Fixed +1 +*ELSET, ELSET=Beam +1 +*MATERIAL, NAME=Steel +*ELASTIC +200.0, 0.25 +*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel +1.0, 1.0, 0.0, 1.0, 1.0 +0.0, 1.0, 0.0 +*STEP +*STATIC +*BOUNDARY +Fixed, 1, 6 +*CLOAD +2, 2, -1.0 +*RESTART, WRITE, FREQUENCY=0 +*OUTPUT, FIELD, VARIABLE=PRESELECT +*OUTPUT, HISTORY, VARIABLE=PRESELECT +*END STEP diff --git a/tests/unit/io/abaqus/input_contract_test.cpp b/tests/unit/io/abaqus/input_contract_test.cpp new file mode 100644 index 0000000..b39ec81 --- /dev/null +++ b/tests/unit/io/abaqus/input_contract_test.cpp @@ -0,0 +1,294 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +struct ContractCase final { + std::string id; + bool valid; + std::filesystem::path fixture; + std::optional expected_stage; + std::string expected_code; + std::size_t expected_line; + std::optional node_count; + std::optional element_count; + std::optional node_set_count; + std::optional element_set_count; + std::optional prescribed_dof_count; + std::optional nodal_load_count; + std::string shear_source; + std::optional shear_area_y; + std::optional shear_area_z; + std::string checked_node_set; + std::string checked_element_set; +}; + +struct ContractOutcome final { + std::optional domain; + std::vector diagnostics; +}; + +std::vector split( + const std::string_view text, + const char delimiter) { + std::vector fields; + std::size_t first = 0; + while (true) { + const std::size_t next = text.find(delimiter, first); + fields.emplace_back( + text.substr( + first, + next == std::string_view::npos + ? std::string_view::npos + : next - first)); + if (next == std::string_view::npos) { + break; + } + first = next + 1U; + } + return fields; +} + +std::optional optional_size(const std::string& field) { + if (field == "-") { + return std::nullopt; + } + return static_cast(std::stoull(field)); +} + +std::optional optional_real(const std::string& field) { + if (field == "-") { + return std::nullopt; + } + return std::stod(field); +} + +std::optional optional_stage( + const std::string& field) { + if (field == "-") { + return std::nullopt; + } + if (field == "io") { + return fesa::DiagnosticStage::io; + } + if (field == "syntax") { + return fesa::DiagnosticStage::syntax; + } + if (field == "semantic") { + return fesa::DiagnosticStage::semantic; + } + if (field == "model") { + return fesa::DiagnosticStage::model; + } + throw std::runtime_error{"Unknown diagnostic stage in contract manifest."}; +} + +std::vector load_contract_cases() { + const std::filesystem::path path = + std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" / + "contract.tsv"; + std::ifstream input{path, std::ios::binary}; + if (!input) { + throw std::runtime_error{"Unable to open Abaqus contract manifest."}; + } + + std::vector cases; + std::string line; + while (std::getline(input, line)) { + if (line.empty() || line.front() == '#') { + continue; + } + const std::vector fields = split(line, '\t'); + if (fields.size() != 17U) { + throw std::runtime_error{ + "Abaqus contract manifest row must contain 17 fields."}; + } + const bool valid = fields[1] == "valid"; + if (!valid && fields[1] != "invalid") { + throw std::runtime_error{ + "Abaqus contract outcome must be valid or invalid."}; + } + cases.push_back({ + fields[0], + valid, + fields[2], + optional_stage(fields[3]), + fields[4] == "-" ? std::string{} : fields[4], + static_cast(std::stoull(fields[5])), + optional_size(fields[6]), + optional_size(fields[7]), + optional_size(fields[8]), + optional_size(fields[9]), + optional_size(fields[10]), + optional_size(fields[11]), + fields[12], + optional_real(fields[13]), + optional_real(fields[14]), + fields[15], + fields[16], + }); + } + if (cases.empty()) { + throw std::runtime_error{"Abaqus contract manifest is empty."}; + } + return cases; +} + +ContractOutcome parse_and_map(const std::filesystem::path& path) { + auto parsed = fesa::parse_deck(path); + if (!parsed.deck.has_value()) { + return {std::nullopt, std::move(parsed.diagnostics)}; + } + auto mapped = fesa::map_deck_to_domain(*parsed.deck); + return {std::move(mapped.domain), std::move(mapped.diagnostics)}; +} + +std::string diagnostics_text( + const std::vector& diagnostics) { + std::ostringstream output; + for (const auto& diagnostic : diagnostics) { + output << diagnostic.code; + if (diagnostic.source.has_value()) { + output << '@' << diagnostic.source->line; + } + output << '\n'; + } + return output.str(); +} + +std::pair> expected_set( + const std::string& field) { + const std::size_t colon = field.find(':'); + if (colon == std::string::npos) { + throw std::runtime_error{"Set check must use name:label,... format."}; + } + std::vector labels; + for (const std::string& label : + split(std::string_view{field}.substr(colon + 1U), ',')) { + labels.push_back(std::stoll(label)); + } + return {field.substr(0, colon), std::move(labels)}; +} + +void expect_node_set( + const fesa::Domain& domain, + const std::string& field) { + if (field == "-") { + return; + } + const auto [name, expected_labels] = expected_set(field); + const auto found = std::ranges::find( + domain.node_sets(), name, &fesa::NodeSet::name); + ASSERT_NE(found, domain.node_sets().end()); + + std::vector actual_labels; + for (const fesa::NodeId member : found->members) { + actual_labels.push_back(domain.node(member).origin.local_label); + } + std::ranges::sort(actual_labels); + EXPECT_EQ(actual_labels, expected_labels); +} + +void expect_element_set( + const fesa::Domain& domain, + const std::string& field) { + if (field == "-") { + return; + } + const auto [name, expected_labels] = expected_set(field); + const auto found = std::ranges::find( + domain.element_sets(), name, &fesa::ElementSet::name); + ASSERT_NE(found, domain.element_sets().end()); + + std::vector actual_labels; + for (const fesa::ElementId member : found->members) { + const auto element = std::ranges::find( + domain.beam_elements(), member, &fesa::BeamElement::id); + ASSERT_NE(element, domain.beam_elements().end()); + actual_labels.push_back(element->origin.local_label); + } + std::ranges::sort(actual_labels); + EXPECT_EQ(actual_labels, expected_labels); +} + +class AbaqusInputContractTest + : public testing::TestWithParam {}; + +TEST_P(AbaqusInputContractTest, FixtureMatchesNormativeContract) { + const ContractCase& contract = GetParam(); + const std::filesystem::path path = + std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" / + contract.fixture; + SCOPED_TRACE(contract.id); + ASSERT_TRUE(std::filesystem::is_regular_file(path)); + + const ContractOutcome outcome = parse_and_map(path); + if (!contract.valid) { + EXPECT_FALSE(outcome.domain.has_value()); + ASSERT_TRUE(contract.expected_stage.has_value()); + const auto diagnostic = std::ranges::find_if( + outcome.diagnostics, + [&contract](const fesa::Diagnostic& candidate) { + return candidate.stage == *contract.expected_stage && + candidate.code == contract.expected_code && + candidate.source.has_value() && + candidate.source->line == contract.expected_line; + }); + EXPECT_NE(diagnostic, outcome.diagnostics.end()) + << "Expected " << contract.expected_code << '@' + << contract.expected_line << " but received:\n" + << diagnostics_text(outcome.diagnostics); + return; + } + + ASSERT_TRUE(outcome.domain.has_value()) + << diagnostics_text(outcome.diagnostics); + ASSERT_TRUE(outcome.diagnostics.empty()) + << diagnostics_text(outcome.diagnostics); + const fesa::Domain& domain = *outcome.domain; + EXPECT_EQ(domain.nodes().size(), *contract.node_count); + EXPECT_EQ(domain.beam_elements().size(), *contract.element_count); + EXPECT_EQ(domain.node_sets().size(), *contract.node_set_count); + EXPECT_EQ(domain.element_sets().size(), *contract.element_set_count); + EXPECT_EQ( + domain.step().prescribed_dofs.size(), + *contract.prescribed_dof_count); + EXPECT_EQ(domain.step().nodal_loads.size(), *contract.nodal_load_count); + + ASSERT_EQ(domain.sections().size(), 1U); + const fesa::BeamSection& section = domain.sections().front(); + const auto expected_source = contract.shear_source == "input" + ? fesa::ShearPropertySource::input + : fesa::ShearPropertySource::phase1_default; + EXPECT_EQ(section.shear_source, expected_source); + EXPECT_NEAR(section.shear_area_y, *contract.shear_area_y, 1.0e-12); + EXPECT_NEAR(section.shear_area_z, *contract.shear_area_z, 1.0e-12); + expect_node_set(domain, contract.checked_node_set); + expect_element_set(domain, contract.checked_element_set); +} + +INSTANTIATE_TEST_SUITE_P( + AbaqusInputContract, + AbaqusInputContractTest, + testing::ValuesIn(load_contract_cases()), + [](const testing::TestParamInfo& info) { + return info.param.id; + }); + +} // namespace