Compare commits
8 Commits
5618636f0d
...
51939fba5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 51939fba5b | |||
| f23deb0ade | |||
| fb67007527 | |||
| 4d04f3dbbe | |||
| 9d37465d92 | |||
| e0a6a6fa70 | |||
| 218cfa9d50 | |||
| 5fe57cb145 |
+154
-76
@@ -1,90 +1,168 @@
|
||||
# FESA HDF5 Schema 1.0.0
|
||||
# FESA HDF5 Schema 2.0.0
|
||||
|
||||
## 1. 범위
|
||||
## 1. Scope and version compatibility
|
||||
|
||||
Schema `1.0.0`은 `results-and-pipeline` Phase의 최소 수직 슬라이스를 정의한다.
|
||||
파일은 활성 `Domain`의 절점, Beam 연결성, 적용된 전단면적과 그 출처, 그리고 전역
|
||||
좌표계 절점 변위·회전 및 반력·반력모멘트를 저장한다. 단위 변환은 수행하지 않는다.
|
||||
Schema `2.0.0` is the self-contained Phase 1 result contract. One file contains
|
||||
the active normalized model, its single linear-static analysis definition and
|
||||
solver settings, and every nodal and Beam result needed without the source
|
||||
`.inp` file. FESA performs no unit conversion.
|
||||
|
||||
이 버전에는 재료 전체 속성, 집합, 하중·경계조건, solver 설정, 요소 결과, history,
|
||||
reference CSV 및 진단 dataset을 저장하지 않는다. 이후 같은 major version에서
|
||||
dataset을 추가할 수 있지만 아래 required object의 의미, 형상 또는 datatype을
|
||||
변경해서는 안 된다.
|
||||
Schema `1.0.0` was the earlier minimal vertical slice. Version `2.0.0` changes
|
||||
the required model and result objects, so it is a new major version rather than
|
||||
an in-place change to `1.0.0`. The current writer and reader accept exactly
|
||||
`2.0.0`; every other version fails with `hdf5.unsupported_schema`. A future
|
||||
reader may explicitly add support for compatible minor versions, but must not
|
||||
infer compatibility from a version prefix.
|
||||
|
||||
## 2. 공통 규칙
|
||||
## 2. Common rules
|
||||
|
||||
- root attribute `schema_version`은 UTF-8 문자열 `1.0.0`이다.
|
||||
- 정수 dataset은 명시한 little-endian 고정폭 타입을 사용한다.
|
||||
- 실수 dataset은 IEEE 754 little-endian 64-bit 타입을 사용한다.
|
||||
- 문자열 dataset과 attribute는 UTF-8 variable-length string을 사용한다.
|
||||
- `dense_index`는 해당 dataset 행의 0-based index이며 연속적이다.
|
||||
- `internal_id`와 결과의 `node_ids`는 FESA semantic model의 nonnegative ID다.
|
||||
- flat/orphan mesh의 `part_name`과 `instance_name`은 빈 문자열이다.
|
||||
- 결과의 6개 component 순서는
|
||||
`(Ux, Uy, Uz, Rx, Ry, Rz)` 및 `(RFx, RFy, RFz, RMx, RMy, RMz)`다.
|
||||
- Step과 frame group 이름은 각각 0부터 연속된 decimal index다. 원래 Step 이름은
|
||||
Step group의 `name` attribute에 저장한다.
|
||||
- Root attributes are variable-length UTF-8 strings:
|
||||
`schema_version="2.0.0"`, `fesa_version`, and
|
||||
`unit_policy="consistent_input_units_no_conversion"`, `input_source`, and
|
||||
`input_fingerprint`. `input_source` is the UTF-8 path supplied to the solve
|
||||
request. `input_fingerprint` is `fnv1a64:` followed by the 16 lowercase
|
||||
hexadecimal digits of FNV-1a 64 over the original input bytes; it is a
|
||||
reproducibility identifier, not a cryptographic integrity guarantee.
|
||||
- Integer datasets use the stated little-endian fixed-width type. Floating
|
||||
datasets use IEEE 754 little-endian `float64`. Strings are variable-length
|
||||
UTF-8.
|
||||
- `dense_index` is a contiguous 0-based row index. Semantic `internal_id`
|
||||
values are nonnegative and are not assumed to be dense or ordered.
|
||||
- Flat/orphan mesh `part_name` and `instance_name` values are empty strings.
|
||||
- Ragged arrays use an offset dataset of length `row_count + 1`. Offsets start
|
||||
at zero, are nondecreasing, and the final offset equals the flattened row
|
||||
count. Input order is preserved.
|
||||
- Numeric field datasets carry UTF-8 `coordinate_system` and `components`
|
||||
attributes where listed. `components` is a comma-separated ordered list.
|
||||
- Step and frame group names are contiguous decimal indices beginning at zero.
|
||||
Phase 1 requires exactly one analysis step and one result step with the same
|
||||
name, and exactly one result frame in that step.
|
||||
|
||||
## 3. Required objects
|
||||
|
||||
```text
|
||||
/
|
||||
├── @schema_version UTF-8 = "1.0.0"
|
||||
├── model
|
||||
│ ├── nodes
|
||||
│ │ ├── dense_index uint64 [node_count]
|
||||
│ │ ├── internal_id int64 [node_count]
|
||||
│ │ ├── part_name UTF-8 [node_count]
|
||||
│ │ ├── instance_name UTF-8 [node_count]
|
||||
│ │ ├── local_label int64 [node_count]
|
||||
│ │ └── coordinates float64[node_count, 3]
|
||||
│ ├── elements
|
||||
│ │ ├── dense_index uint64 [element_count]
|
||||
│ │ ├── internal_id int64 [element_count]
|
||||
│ │ ├── connectivity uint64 [element_count, 2]
|
||||
│ │ └── section_id int64 [element_count]
|
||||
│ └── sections
|
||||
│ ├── internal_id int64 [section_count]
|
||||
│ ├── shear_area_y float64[section_count]
|
||||
│ ├── shear_area_z float64[section_count]
|
||||
│ └── shear_source uint8 [section_count]
|
||||
└── results
|
||||
└── steps
|
||||
└── <step_index>
|
||||
├── @name UTF-8
|
||||
└── frames
|
||||
└── <frame_index>
|
||||
├── @step_time float64
|
||||
└── nodal
|
||||
├── node_ids int64 [result_node_count]
|
||||
├── displacement float64[result_node_count, 6]
|
||||
└── reaction float64[result_node_count, 6]
|
||||
```
|
||||
### 3.1 Model
|
||||
|
||||
`model/elements/connectivity`는 `model/nodes/dense_index`를 참조한다. 따라서
|
||||
`internal_id`가 연속적이거나 Domain 저장 순서와 같다고 가정하지 않는다.
|
||||
`section_id`는 `model/sections/internal_id`를 참조한다.
|
||||
Let `N`, `E`, `M`, `S`, `NS`, and `ES` be the node, Beam element, material,
|
||||
section, node-set, and element-set counts. Let `P` be the total number of
|
||||
section recovery points, `NM` the total node-set membership count, and `EM` the
|
||||
total element-set membership count.
|
||||
|
||||
`shear_source` 값은 다음과 같다.
|
||||
| Path | Type | Rank and shape | Attributes / meaning |
|
||||
|---|---|---|---|
|
||||
| `/model/nodes/dense_index` | `uint64` | 1, `[N]` | contiguous row index |
|
||||
| `/model/nodes/internal_id` | `int64` | 1, `[N]` | `NodeId` |
|
||||
| `/model/nodes/part_name` | UTF-8 | 1, `[N]` | entity provenance |
|
||||
| `/model/nodes/instance_name` | UTF-8 | 1, `[N]` | entity provenance |
|
||||
| `/model/nodes/local_label` | `int64` | 1, `[N]` | external local label |
|
||||
| `/model/nodes/coordinates` | `float64` | 2, `[N,3]` | `coordinate_system="global"`, `components="X,Y,Z"` |
|
||||
| `/model/elements/dense_index` | `uint64` | 1, `[E]` | contiguous row index |
|
||||
| `/model/elements/internal_id` | `int64` | 1, `[E]` | `ElementId` |
|
||||
| `/model/elements/part_name` | UTF-8 | 1, `[E]` | entity provenance |
|
||||
| `/model/elements/instance_name` | UTF-8 | 1, `[E]` | entity provenance |
|
||||
| `/model/elements/local_label` | `int64` | 1, `[E]` | external local label |
|
||||
| `/model/elements/connectivity` | `uint64` | 2, `[E,2]` | node `dense_index`, ordered end `-1,+1` |
|
||||
| `/model/elements/material_id` | `int64` | 1, `[E]` | references material `internal_id` |
|
||||
| `/model/elements/section_id` | `int64` | 1, `[E]` | references section `internal_id` |
|
||||
| `/model/materials/internal_id` | `int64` | 1, `[M]` | `MaterialId` |
|
||||
| `/model/materials/name` | UTF-8 | 1, `[M]` | material name |
|
||||
| `/model/materials/young_modulus` | `float64` | 1, `[M]` | finite, positive |
|
||||
| `/model/materials/poisson_ratio` | `float64` | 1, `[M]` | finite, `-1 < nu < 0.5` |
|
||||
| `/model/sections/internal_id` | `int64` | 1, `[S]` | `SectionId` |
|
||||
| `/model/sections/name` | UTF-8 | 1, `[S]` | section name |
|
||||
| `/model/sections/area` | `float64` | 1, `[S]` | `A` |
|
||||
| `/model/sections/moment_y` | `float64` | 1, `[S]` | `Iy` |
|
||||
| `/model/sections/moment_z` | `float64` | 1, `[S]` | `Iz` |
|
||||
| `/model/sections/torsion_constant` | `float64` | 1, `[S]` | `J` |
|
||||
| `/model/sections/shear_area_y` | `float64` | 1, `[S]` | applied `Asy` |
|
||||
| `/model/sections/shear_area_z` | `float64` | 1, `[S]` | applied `Asz` |
|
||||
| `/model/sections/shear_source` | `uint8` | 1, `[S]` | `0=input`, `1=phase1_default` |
|
||||
| `/model/sections/orientation` | `float64` | 2, `[S,3]` | `coordinate_system="global"`, `components="X,Y,Z"` |
|
||||
| `/model/sections/recovery_point_offsets` | `uint64` | 1, `[S+1]` | offsets into `recovery_points` |
|
||||
| `/model/sections/recovery_points` | `float64` | 2, `[P,2]` | `coordinate_system="element_local"`, `components="y,z"` |
|
||||
| `/model/sets/node/names` | UTF-8 | 1, `[NS]` | exact node-set names |
|
||||
| `/model/sets/node/member_offsets` | `uint64` | 1, `[NS+1]` | offsets into `members` |
|
||||
| `/model/sets/node/members` | `int64` | 1, `[NM]` | `NodeId`, input set/member order |
|
||||
| `/model/sets/element/names` | UTF-8 | 1, `[ES]` | exact element-set names |
|
||||
| `/model/sets/element/member_offsets` | `uint64` | 1, `[ES+1]` | offsets into `members` |
|
||||
| `/model/sets/element/members` | `int64` | 1, `[EM]` | `ElementId`, input set/member order |
|
||||
|
||||
| 값 | 의미 |
|
||||
### 3.2 Analysis
|
||||
|
||||
`/analysis/steps/0` has UTF-8 attribute `name`. Let `B` be the prescribed-DOF
|
||||
count and `L` the nodal-load count.
|
||||
|
||||
| Path | Type | Rank and shape | Attributes / meaning |
|
||||
|---|---|---|---|
|
||||
| `/analysis/steps/0/boundary_conditions/node_ids` | `int64` | 1, `[B]` | target `NodeId` |
|
||||
| `/analysis/steps/0/boundary_conditions/dofs` | `uint8` | 1, `[B]` | Abaqus/FESA DOF number 1 through 6 |
|
||||
| `/analysis/steps/0/boundary_conditions/values` | `float64` | 1, `[B]` | prescribed value |
|
||||
| `/analysis/steps/0/nodal_loads/node_ids` | `int64` | 1, `[L]` | target `NodeId` |
|
||||
| `/analysis/steps/0/nodal_loads/values` | `float64` | 2, `[L,6]` | `coordinate_system="global"`, `components="Fx,Fy,Fz,Mx,My,Mz"` |
|
||||
|
||||
`/analysis/solver_settings` has the exact UTF-8 attributes used by the Phase 1
|
||||
pipeline: `backend="mkl_pardiso"`,
|
||||
`matrix_storage="symmetric_upper_csr"`,
|
||||
`matrix_type="symmetric_positive_definite"`,
|
||||
`constraint_method="essential_dof_elimination"`, and
|
||||
`assembly="deterministic_serial"`. No unused future settings are stored.
|
||||
|
||||
### 3.3 Results
|
||||
|
||||
`/results/steps/0` has UTF-8 attribute `name`; frame group `0` has scalar
|
||||
`float64` attribute `step_time`. Let `RN`, `RE`, `RP`, and `D` be the nodal
|
||||
result, Beam result, flattened Beam recovery-point, and diagnostic counts.
|
||||
|
||||
| Path below `/results/steps/0/frames/0` | Type | Rank and shape | Attributes / meaning |
|
||||
|---|---|---|---|
|
||||
| `nodal/node_ids` | `int64` | 1, `[RN]` | `NodeId`; provenance is joined from `/model/nodes` |
|
||||
| `nodal/displacement` | `float64` | 2, `[RN,6]` | `coordinate_system="global"`, `components="Ux,Uy,Uz,Rx,Ry,Rz"` |
|
||||
| `nodal/reaction` | `float64` | 2, `[RN,6]` | `coordinate_system="global"`, `components="RFx,RFy,RFz,RMx,RMy,RMz"` |
|
||||
| `element/beam/element_ids` | `int64` | 1, `[RE]` | `ElementId` |
|
||||
| `element/beam/part_name` | UTF-8 | 1, `[RE]` | result provenance |
|
||||
| `element/beam/instance_name` | UTF-8 | 1, `[RE]` | result provenance |
|
||||
| `element/beam/local_label` | `int64` | 1, `[RE]` | result provenance |
|
||||
| `element/beam/local_frame` | `float64` | 3, `[RE,3,3]` | `coordinate_system="global"`, `components="ex,ey,ez"`; last dimension is `X,Y,Z` |
|
||||
| `element/beam/end_node_ids` | `int64` | 2, `[RE,2]` | ordered ends `-1,+1` |
|
||||
| `element/beam/xi` | `float64` | 2, `[RE,2]` | `components="end_minus,end_plus"` |
|
||||
| `element/beam/section_strain` | `float64` | 3, `[RE,2,6]` | `coordinate_system="element_local"`, `components="epsilon,gamma_y,gamma_z,kappa_x,kappa_y,kappa_z"` |
|
||||
| `element/beam/section_force` | `float64` | 3, `[RE,2,6]` | `coordinate_system="element_local"`, `components="N,Vy,Vz,T,My,Mz"` |
|
||||
| `element/beam/centroid_sigma_xx` | `float64` | 2, `[RE,2]` | `coordinate_system="element_local"`, `components="end_minus,end_plus"`, `quantity="sigma_xx"` |
|
||||
| `element/beam/recovery_point_offsets` | `uint64` | 1, `[RE+1]` | offsets into recovery-point rows |
|
||||
| `element/beam/recovery_point_sigma_xx` | `float64` | 2, `[RP,2]` | `coordinate_system="element_local"`, `components="end_minus,end_plus"`, `quantity="sigma_xx"`; point order comes from the referenced section |
|
||||
| `diagnostics/stage` | `uint8` | 1, `[D]` | enum table below |
|
||||
| `diagnostics/severity` | `uint8` | 1, `[D]` | `0=warning`, `1=error` |
|
||||
| `diagnostics/code` | UTF-8 | 1, `[D]` | exact diagnostic code |
|
||||
| `diagnostics/message` | UTF-8 | 1, `[D]` | exact diagnostic message |
|
||||
| `diagnostics/has_source` | `uint8` | 1, `[D]` | `0=no source`, `1=source present` |
|
||||
| `diagnostics/source_file` | UTF-8 | 1, `[D]` | empty when source is absent |
|
||||
| `diagnostics/source_line` | `uint64` | 1, `[D]` | zero when source is absent |
|
||||
| `diagnostics/source_column` | `uint64` | 1, `[D]` | zero when source is absent |
|
||||
|
||||
Diagnostic stage encoding follows the declaration order:
|
||||
|
||||
| Value | Stage |
|
||||
|---:|---|
|
||||
| `0` | 입력에서 명시된 전단강성으로부터 구성한 값 (`input`) |
|
||||
| `1` | Phase 1 기본값 `Asy=Asz=5A/6`, `SCF=0` (`phase1_default`) |
|
||||
| 0 | `io` |
|
||||
| 1 | `syntax` |
|
||||
| 2 | `semantic` |
|
||||
| 3 | `model` |
|
||||
| 4 | `equation` |
|
||||
| 5 | `solver` |
|
||||
| 6 | `results` |
|
||||
| 7 | `validation` |
|
||||
|
||||
## 4. Writer와 reader 계약
|
||||
## 4. Writer and reader contract
|
||||
|
||||
- writer는 쓰기 전에 `ResultDatabase` 유효성과 schema version을 검사한다.
|
||||
- schema `1.0.0`이 표현하지 않는 non-empty frame diagnostics는 파일을 만들기 전에
|
||||
`hdf5.unsupported_result_diagnostics`로 거부한다.
|
||||
- required object 생성·쓰기·flush·close 중 HDF5 오류가 발생하면 성공으로 반환하지
|
||||
않고 `DiagnosticStage::results` 오류로 변환한다.
|
||||
- reader는 schema version, required object, datatype, rank와 shape를 검사한다.
|
||||
- reader는 model internal ID의 uniqueness, finite coordinates 및 finite positive
|
||||
shear area를 검사한다.
|
||||
- writer와 reader는 모든 nodal result ID가 `model/nodes/internal_id`에 존재하는지
|
||||
검사하며, 없는 ID를 성공 결과로 반환하지 않는다.
|
||||
- reader는 nodal result를 `ResultDatabase`로, model dataset을 HDF5 adapter 전용
|
||||
read-only inspection model로 반환한다. `Domain`과 `ResultDatabase`에는 HDF5
|
||||
저장 계약을 추가하지 않는다.
|
||||
- malformed 또는 지원하지 않는 파일은 부분 database를 반환하지 않는다.
|
||||
- The writer validates `ResultDatabase`, exact schema version, model/result ID
|
||||
and provenance joins, Beam connectivity, recovery-point counts, and the
|
||||
single-step name before creating the file.
|
||||
- Required-object creation, write, flush, and close failures become
|
||||
`DiagnosticStage::results` errors. A failed write is never reported as
|
||||
success.
|
||||
- The reader validates the exact version, required datatypes, ranks, shapes,
|
||||
offsets, finite values, uniqueness, references, field metadata, and result
|
||||
contracts. It does not return a partial database or partial snapshots.
|
||||
- The public reader returns adapter-owned, read-only metadata, model, and
|
||||
analysis snapshots plus the semantic `ResultDatabase`. No HDF5 object or
|
||||
handle escapes the adapter, and the snapshots contain enough information to
|
||||
reconstruct the Phase 1 model and run definition without the source deck.
|
||||
|
||||
@@ -2,22 +2,34 @@
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/core/vec3.hpp>
|
||||
#include <fesa/fem/beam_frame.hpp>
|
||||
#include <fesa/model/beam_section.hpp>
|
||||
#include <fesa/model/ids.hpp>
|
||||
#include <fesa/model/material.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct Beam3D2Input final {
|
||||
std::array<Vec3, 2> coordinates;
|
||||
std::array<NodeId, 2> node_ids;
|
||||
IsotropicElastic material;
|
||||
BeamSection section;
|
||||
};
|
||||
|
||||
struct BeamSectionResult final {
|
||||
double xi;
|
||||
NodeId end_node;
|
||||
std::array<double, 6> section_strain;
|
||||
std::array<double, 6> section_force;
|
||||
double centroid_sigma_xx;
|
||||
std::vector<double> sigma_xx;
|
||||
};
|
||||
|
||||
struct Beam3D2Contribution final {
|
||||
Matrix12 local_stiffness;
|
||||
Matrix12 global_stiffness;
|
||||
@@ -32,4 +44,9 @@ struct BeamKernelResult final {
|
||||
[[nodiscard]] BeamKernelResult compute_beam3d2(
|
||||
const Beam3D2Input& input);
|
||||
|
||||
[[nodiscard]] std::vector<BeamSectionResult> recover_beam3d2(
|
||||
const Beam3D2Input& input,
|
||||
std::span<const double, 12> element_displacement,
|
||||
std::span<const std::array<double, 2>> recovery_points);
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
@@ -12,6 +13,7 @@ namespace fesa {
|
||||
struct ParseDeckResult final {
|
||||
std::optional<ParsedDeck> deck;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
std::string input_fingerprint;
|
||||
};
|
||||
|
||||
[[nodiscard]] ParseDeckResult parse_deck(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
@@ -26,33 +27,74 @@ struct Hdf5NodeSnapshot final {
|
||||
struct Hdf5ElementSnapshot final {
|
||||
std::uint64_t dense_index;
|
||||
ElementId id;
|
||||
EntityOrigin origin;
|
||||
std::array<std::uint64_t, 2> connectivity;
|
||||
MaterialId material;
|
||||
SectionId section;
|
||||
};
|
||||
|
||||
struct Hdf5SectionSnapshot final {
|
||||
SectionId id;
|
||||
std::string name;
|
||||
double area;
|
||||
double iy;
|
||||
double iz;
|
||||
double torsion_j;
|
||||
double shear_area_y;
|
||||
double shear_area_z;
|
||||
ShearPropertySource shear_source;
|
||||
Vec3 orientation;
|
||||
std::vector<std::array<double, 2>> recovery_points;
|
||||
};
|
||||
|
||||
struct Hdf5ModelSnapshot final {
|
||||
std::vector<Hdf5NodeSnapshot> nodes;
|
||||
std::vector<Hdf5ElementSnapshot> elements;
|
||||
std::vector<IsotropicElastic> materials;
|
||||
std::vector<Hdf5SectionSnapshot> sections;
|
||||
std::vector<NodeSet> node_sets;
|
||||
std::vector<ElementSet> element_sets;
|
||||
};
|
||||
|
||||
struct Hdf5MetadataSnapshot final {
|
||||
std::string schema_version;
|
||||
std::string fesa_version;
|
||||
std::string unit_policy;
|
||||
std::string input_source;
|
||||
std::string input_fingerprint;
|
||||
};
|
||||
|
||||
struct Hdf5InputIdentity final {
|
||||
std::string source;
|
||||
std::string fingerprint;
|
||||
};
|
||||
|
||||
struct Hdf5SolverSettingsSnapshot final {
|
||||
std::string backend;
|
||||
std::string matrix_storage;
|
||||
std::string matrix_type;
|
||||
std::string constraint_method;
|
||||
std::string assembly;
|
||||
};
|
||||
|
||||
struct Hdf5AnalysisSnapshot final {
|
||||
StepDefinition step;
|
||||
Hdf5SolverSettingsSnapshot solver;
|
||||
};
|
||||
|
||||
struct Hdf5ReadResult final {
|
||||
std::optional<ResultDatabase> database;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
std::optional<Hdf5ModelSnapshot> model;
|
||||
std::optional<Hdf5MetadataSnapshot> metadata;
|
||||
std::optional<Hdf5AnalysisSnapshot> analysis;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
||||
const std::filesystem::path& path,
|
||||
const Domain& domain,
|
||||
const ResultDatabase& database);
|
||||
const ResultDatabase& database,
|
||||
const Hdf5InputIdentity& input_identity);
|
||||
|
||||
[[nodiscard]] Hdf5ReadResult read_hdf5_results(
|
||||
const std::filesystem::path& path);
|
||||
|
||||
@@ -2,23 +2,62 @@
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/core/status.hpp>
|
||||
#include <fesa/elements/beam/beam3d2.hpp>
|
||||
#include <fesa/model/entity_origin.hpp>
|
||||
#include <fesa/model/ids.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
enum class FieldCoordinateSystem { global, element_local };
|
||||
|
||||
struct NodalFrame final {
|
||||
inline static constexpr FieldCoordinateSystem coordinate_system =
|
||||
FieldCoordinateSystem::global;
|
||||
inline static constexpr std::array<std::string_view, 6>
|
||||
displacement_components{
|
||||
"Ux", "Uy", "Uz", "Rx", "Ry", "Rz"};
|
||||
inline static constexpr std::array<std::string_view, 6>
|
||||
reaction_components{
|
||||
"RFx", "RFy", "RFz", "RMx", "RMy", "RMz"};
|
||||
|
||||
std::vector<NodeId> node_ids;
|
||||
std::vector<EntityOrigin> origins;
|
||||
std::vector<std::array<double, 6>> displacement;
|
||||
std::vector<std::array<double, 6>> reaction;
|
||||
};
|
||||
|
||||
struct BeamElementFrame final {
|
||||
inline static constexpr FieldCoordinateSystem coordinate_system =
|
||||
FieldCoordinateSystem::element_local;
|
||||
inline static constexpr std::array<std::string_view, 6>
|
||||
section_strain_components{
|
||||
"epsilon", "gamma_y", "gamma_z", "kappa_x", "kappa_y",
|
||||
"kappa_z"};
|
||||
inline static constexpr std::array<std::string_view, 6>
|
||||
section_force_components{
|
||||
"N", "Vy", "Vz", "T", "My", "Mz"};
|
||||
inline static constexpr std::string_view axial_stress_component =
|
||||
"sigma_xx";
|
||||
|
||||
ElementId element;
|
||||
EntityOrigin origin;
|
||||
BeamFrame local_frame;
|
||||
std::array<BeamSectionResult, 2> end_results;
|
||||
};
|
||||
|
||||
struct ElementFrame final {
|
||||
std::vector<BeamElementFrame> beams;
|
||||
};
|
||||
|
||||
struct ResultFrame final {
|
||||
double step_time;
|
||||
NodalFrame nodal;
|
||||
ElementFrame element;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@
|
||||
},
|
||||
{
|
||||
"dir": "result-contract-completion",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"completed_at": "2026-08-02T01:51:25+0900"
|
||||
},
|
||||
{
|
||||
"dir": "beam-reference-qualification",
|
||||
|
||||
@@ -5,17 +5,28 @@
|
||||
{
|
||||
"step": 0,
|
||||
"name": "beam-element-end-recovery",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Added actual Beam node IDs and signed two-end strain, resultant, centroid/recovery-point stress recovery using the stiffness frame and reduced-shear convention, with hand-calculated tests.",
|
||||
"started_at": "2026-08-02T00:27:47+0900",
|
||||
"completed_at": "2026-08-02T01:14:40+0900"
|
||||
},
|
||||
{
|
||||
"step": 1,
|
||||
"name": "complete-result-contract",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Added nodal and Beam element provenance, explicit field metadata and validation, and production element-end recovery orchestration in LinearStaticAnalysis.",
|
||||
"started_at": "2026-08-02T01:14:43+0900",
|
||||
"completed_at": "2026-08-02T01:28:37+0900"
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"name": "self-contained-hdf5",
|
||||
"status": "pending"
|
||||
"status": "completed",
|
||||
"summary": "Defined schema 2.0.0 and completed self-contained HDF5 model, analysis, nodal/Beam result, diagnostic writer/reader round trips with strict metadata and version validation.",
|
||||
"started_at": "2026-08-02T01:28:37+0900",
|
||||
"completed_at": "2026-08-02T01:51:24+0900"
|
||||
}
|
||||
]
|
||||
],
|
||||
"created_at": "2026-08-02T00:27:47+0900",
|
||||
"completed_at": "2026-08-02T01:51:25+0900"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -12,6 +13,7 @@
|
||||
#include <fesa/assembly/equation_system.hpp>
|
||||
#include <fesa/assembly/serial_assembler.hpp>
|
||||
#include <fesa/constraints/essential_bc.hpp>
|
||||
#include <fesa/elements/beam/beam3d2.hpp>
|
||||
#include <fesa/fem/dof_manager.hpp>
|
||||
#include <fesa/solvers/linear/pardiso_linear_solver.hpp>
|
||||
|
||||
@@ -34,6 +36,18 @@ AnalysisRunResult equation_failure(
|
||||
}});
|
||||
}
|
||||
|
||||
AnalysisRunResult results_failure(
|
||||
std::string code,
|
||||
std::string message) {
|
||||
return failure({{
|
||||
DiagnosticStage::results,
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
std::nullopt,
|
||||
}});
|
||||
}
|
||||
|
||||
NodalFrame build_nodal_frame(
|
||||
const Domain& domain,
|
||||
const DofManager& dofs,
|
||||
@@ -48,9 +62,11 @@ NodalFrame build_nodal_frame(
|
||||
|
||||
NodalFrame nodal;
|
||||
nodal.node_ids = std::move(node_ids);
|
||||
nodal.origins.reserve(nodal.node_ids.size());
|
||||
nodal.displacement.reserve(nodal.node_ids.size());
|
||||
nodal.reaction.reserve(nodal.node_ids.size());
|
||||
for (const NodeId node_id : nodal.node_ids) {
|
||||
nodal.origins.push_back(domain.node(node_id).origin);
|
||||
std::array<double, 6> node_displacement{};
|
||||
std::array<double, 6> node_reaction{};
|
||||
for (std::size_t component = 0; component < 6; ++component) {
|
||||
@@ -67,6 +83,70 @@ NodalFrame build_nodal_frame(
|
||||
return nodal;
|
||||
}
|
||||
|
||||
ElementFrame build_element_frame(
|
||||
const Domain& domain,
|
||||
const DofManager& dofs,
|
||||
const std::vector<double>& displacement) {
|
||||
std::vector<const BeamElement*> elements;
|
||||
elements.reserve(domain.beam_elements().size());
|
||||
for (const BeamElement& element : domain.beam_elements()) {
|
||||
elements.push_back(&element);
|
||||
}
|
||||
std::ranges::sort(
|
||||
elements,
|
||||
{},
|
||||
[](const BeamElement* element) {
|
||||
return element->id.value();
|
||||
});
|
||||
|
||||
ElementFrame frame;
|
||||
frame.beams.reserve(elements.size());
|
||||
for (const BeamElement* element : elements) {
|
||||
const Beam3D2Input input{
|
||||
{
|
||||
domain.node(element->nodes[0]).position,
|
||||
domain.node(element->nodes[1]).position,
|
||||
},
|
||||
element->nodes,
|
||||
domain.material(element->material),
|
||||
domain.section(element->section),
|
||||
};
|
||||
const BeamKernelResult kernel = compute_beam3d2(input);
|
||||
if (!kernel.contribution.has_value()) {
|
||||
const std::string message = kernel.diagnostics.empty()
|
||||
? "Beam recovery requires a valid element input."
|
||||
: kernel.diagnostics.front().message;
|
||||
throw std::runtime_error{message};
|
||||
}
|
||||
|
||||
std::array<double, 12> element_displacement{};
|
||||
const std::array<std::size_t, 12> full_dofs =
|
||||
dofs.element_full_dofs(*element);
|
||||
for (std::size_t local = 0; local < full_dofs.size(); ++local) {
|
||||
element_displacement[local] = displacement[full_dofs[local]];
|
||||
}
|
||||
std::vector<BeamSectionResult> recovered = recover_beam3d2(
|
||||
input,
|
||||
element_displacement,
|
||||
input.section.recovery_points);
|
||||
if (recovered.size() != 2U) {
|
||||
throw std::logic_error{
|
||||
"Beam recovery must return exactly two end results."};
|
||||
}
|
||||
|
||||
frame.beams.push_back({
|
||||
element->id,
|
||||
element->origin,
|
||||
kernel.contribution->frame,
|
||||
{
|
||||
std::move(recovered[0]),
|
||||
std::move(recovered[1]),
|
||||
},
|
||||
});
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
||||
@@ -114,14 +194,23 @@ AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
||||
"equation.reaction_recovery_failed", error.what());
|
||||
}
|
||||
|
||||
ElementFrame element;
|
||||
try {
|
||||
element = build_element_frame(domain, dofs, displacement);
|
||||
} catch (const std::exception& error) {
|
||||
return results_failure(
|
||||
"results.element_recovery_failed", error.what());
|
||||
}
|
||||
|
||||
ResultDatabase database{
|
||||
"1.0.0",
|
||||
"2.0.0",
|
||||
{{
|
||||
domain.step().name,
|
||||
{{
|
||||
1.0,
|
||||
build_nodal_frame(
|
||||
domain, dofs, displacement, reaction),
|
||||
std::move(element),
|
||||
{},
|
||||
}},
|
||||
}},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <fesa/analysis/run_solver.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
@@ -8,6 +9,14 @@
|
||||
#include <fesa/io/hdf5/writer.hpp>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
std::string path_utf8(const std::filesystem::path& path) {
|
||||
const std::u8string value = path.u8string();
|
||||
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
||||
ParseDeckResult parsed = parse_deck(request.input_path);
|
||||
@@ -20,13 +29,18 @@ AnalysisRunResult run_solver(const AnalysisRequest& request) {
|
||||
return {false, std::nullopt, std::move(mapped.diagnostics)};
|
||||
}
|
||||
|
||||
const Hdf5InputIdentity identity{
|
||||
path_utf8(request.input_path),
|
||||
parsed.input_fingerprint,
|
||||
};
|
||||
|
||||
AnalysisRunResult run = LinearStaticAnalysis{}.run(*mapped.domain);
|
||||
if (!run.succeeded || !run.results.has_value()) {
|
||||
return run;
|
||||
}
|
||||
|
||||
std::vector<Diagnostic> write_diagnostics = write_hdf5(
|
||||
request.output_path, *mapped.domain, *run.results);
|
||||
request.output_path, *mapped.domain, *run.results, identity);
|
||||
if (!write_diagnostics.empty()) {
|
||||
return {false, std::nullopt, std::move(write_diagnostics)};
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ ElementEvaluation evaluate_element(
|
||||
domain.node(element.nodes[0]).position,
|
||||
domain.node(element.nodes[1]).position,
|
||||
},
|
||||
element.nodes,
|
||||
domain.material(element.material),
|
||||
domain.section(element.section),
|
||||
});
|
||||
|
||||
@@ -128,6 +128,7 @@ std::vector<NumericContribution> collect_numeric_contributions(
|
||||
domain.node(element.nodes[0]).position,
|
||||
domain.node(element.nodes[1]).position,
|
||||
},
|
||||
element.nodes,
|
||||
domain.material(element.material),
|
||||
domain.section(element.section),
|
||||
});
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/fem/gauss_rule.hpp>
|
||||
#include <fesa/fem/line2_shape.hpp>
|
||||
@@ -32,6 +34,20 @@ bool is_positive_finite(const double value) {
|
||||
return std::isfinite(value) && value > 0.0;
|
||||
}
|
||||
|
||||
std::array<double, 6> constitutive_values(
|
||||
const Beam3D2Input& input) {
|
||||
const double shear_modulus =
|
||||
input.material.young / (2.0 * (1.0 + input.material.poisson));
|
||||
return {
|
||||
input.material.young * input.section.area,
|
||||
shear_modulus * input.section.shear_area_y,
|
||||
shear_modulus * input.section.shear_area_z,
|
||||
shear_modulus * input.section.torsion_j,
|
||||
input.material.young * input.section.iy,
|
||||
input.material.young * input.section.iz,
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<BeamKernelResult> validate_properties(
|
||||
const Beam3D2Input& input) {
|
||||
if (!std::isfinite(input.material.young) ||
|
||||
@@ -149,6 +165,34 @@ Matrix12 transform_stiffness(
|
||||
return global;
|
||||
}
|
||||
|
||||
std::array<double, 12> transform_displacement(
|
||||
const Matrix12& transformation,
|
||||
const std::span<const double, 12> global_displacement) {
|
||||
std::array<double, 12> local_displacement{};
|
||||
for (std::size_t row = 0; row < transformation.size(); ++row) {
|
||||
for (std::size_t column = 0;
|
||||
column < transformation[row].size();
|
||||
++column) {
|
||||
local_displacement[row] +=
|
||||
transformation[row][column] * global_displacement[column];
|
||||
}
|
||||
}
|
||||
return local_displacement;
|
||||
}
|
||||
|
||||
std::array<double, 6> evaluate_strain(
|
||||
const StrainMatrix& strain_matrix,
|
||||
const std::array<double, 12>& local_displacement) {
|
||||
std::array<double, 6> strain{};
|
||||
for (std::size_t component = 0; component < strain.size(); ++component) {
|
||||
for (std::size_t dof = 0; dof < local_displacement.size(); ++dof) {
|
||||
strain[component] +=
|
||||
strain_matrix[component][dof] * local_displacement[dof];
|
||||
}
|
||||
}
|
||||
return strain;
|
||||
}
|
||||
|
||||
bool is_finite(const Matrix12& matrix) {
|
||||
for (const auto& row : matrix) {
|
||||
for (const double value : row) {
|
||||
@@ -189,16 +233,8 @@ BeamKernelResult compute_beam3d2(const Beam3D2Input& input) {
|
||||
"Beam kernel requires a representable positive Jacobian.");
|
||||
}
|
||||
|
||||
const double shear_modulus =
|
||||
input.material.young / (2.0 * (1.0 + input.material.poisson));
|
||||
const std::array<double, 6> constitutive{
|
||||
input.material.young * input.section.area,
|
||||
shear_modulus * input.section.shear_area_y,
|
||||
shear_modulus * input.section.shear_area_z,
|
||||
shear_modulus * input.section.torsion_j,
|
||||
input.material.young * input.section.iy,
|
||||
input.material.young * input.section.iz,
|
||||
};
|
||||
const std::array<double, 6> constitutive =
|
||||
constitutive_values(input);
|
||||
for (const double value : constitutive) {
|
||||
if (!is_positive_finite(value)) {
|
||||
return error_result(
|
||||
@@ -241,4 +277,73 @@ BeamKernelResult compute_beam3d2(const Beam3D2Input& input) {
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<BeamSectionResult> recover_beam3d2(
|
||||
const Beam3D2Input& input,
|
||||
const std::span<const double, 12> element_displacement,
|
||||
const std::span<const std::array<double, 2>> recovery_points) {
|
||||
const BeamKernelResult kernel = compute_beam3d2(input);
|
||||
if (!kernel.contribution.has_value()) {
|
||||
const std::string message = kernel.diagnostics.empty()
|
||||
? "Beam recovery requires a valid Beam3D2 input."
|
||||
: kernel.diagnostics.front().message;
|
||||
throw std::invalid_argument{message};
|
||||
}
|
||||
|
||||
const Vec3 axis{
|
||||
input.coordinates[1].x - input.coordinates[0].x,
|
||||
input.coordinates[1].y - input.coordinates[0].y,
|
||||
input.coordinates[1].z - input.coordinates[0].z,
|
||||
};
|
||||
const double jacobian = std::hypot(axis.x, axis.y, axis.z) / 2.0;
|
||||
const Matrix12 transformation =
|
||||
beam_transformation(kernel.contribution->frame);
|
||||
const std::array<double, 12> local_displacement =
|
||||
transform_displacement(transformation, element_displacement);
|
||||
const std::array<double, 6> center_strain = evaluate_strain(
|
||||
strain_matrix(0.0, jacobian),
|
||||
local_displacement);
|
||||
const std::array<double, 6> constitutive =
|
||||
constitutive_values(input);
|
||||
|
||||
std::vector<BeamSectionResult> results;
|
||||
results.reserve(input.node_ids.size());
|
||||
for (std::size_t end = 0; end < input.node_ids.size(); ++end) {
|
||||
const double xi = end == 0 ? -1.0 : 1.0;
|
||||
std::array<double, 6> section_strain = evaluate_strain(
|
||||
strain_matrix(xi, jacobian),
|
||||
local_displacement);
|
||||
section_strain[1] = center_strain[1];
|
||||
section_strain[2] = center_strain[2];
|
||||
|
||||
std::array<double, 6> section_force{};
|
||||
for (std::size_t component = 0;
|
||||
component < section_force.size();
|
||||
++component) {
|
||||
section_force[component] =
|
||||
constitutive[component] * section_strain[component];
|
||||
}
|
||||
|
||||
std::vector<double> sigma_xx;
|
||||
sigma_xx.reserve(recovery_points.size());
|
||||
for (const auto& point : recovery_points) {
|
||||
const double y = point[0];
|
||||
const double z = point[1];
|
||||
sigma_xx.push_back(
|
||||
input.material.young *
|
||||
(section_strain[0] + z * section_strain[4] -
|
||||
y * section_strain[5]));
|
||||
}
|
||||
|
||||
results.push_back({
|
||||
xi,
|
||||
input.node_ids[end],
|
||||
section_strain,
|
||||
section_force,
|
||||
section_force[0] / input.section.area,
|
||||
std::move(sigma_xx),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
@@ -50,6 +53,20 @@ std::string uppercase_ascii(std::string value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string input_fingerprint(const std::string_view bytes) {
|
||||
std::uint64_t fingerprint = 14695981039346656037ULL;
|
||||
for (const char byte : bytes) {
|
||||
fingerprint ^=
|
||||
static_cast<std::uint8_t>(static_cast<unsigned char>(byte));
|
||||
fingerprint *= 1099511628211ULL;
|
||||
}
|
||||
|
||||
std::ostringstream encoded;
|
||||
encoded << "fnv1a64:" << std::hex << std::setfill('0')
|
||||
<< std::setw(16) << fingerprint;
|
||||
return encoded.str();
|
||||
}
|
||||
|
||||
std::vector<std::string> split_fields(const std::string_view value) {
|
||||
std::vector<std::string> fields;
|
||||
std::size_t first = 0;
|
||||
@@ -374,14 +391,26 @@ ParseDeckResult missing_parameter(
|
||||
} // namespace
|
||||
|
||||
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
std::ifstream input{path, std::ios::binary};
|
||||
if (!input) {
|
||||
std::ifstream file{path, std::ios::binary};
|
||||
if (!file) {
|
||||
return failure(
|
||||
DiagnosticStage::io,
|
||||
"abaqus.io.open_failed",
|
||||
"Unable to open Abaqus input file.",
|
||||
SourceLocation{path, 0U, 0U});
|
||||
}
|
||||
const std::string source_bytes{
|
||||
std::istreambuf_iterator<char>{file},
|
||||
std::istreambuf_iterator<char>{},
|
||||
};
|
||||
if (file.bad()) {
|
||||
return failure(
|
||||
DiagnosticStage::io,
|
||||
"abaqus.io.read_failed",
|
||||
"Failed while reading Abaqus input file.",
|
||||
SourceLocation{path, 0U, 0U});
|
||||
}
|
||||
std::istringstream input{source_bytes};
|
||||
|
||||
ParsedDeck deck;
|
||||
Scope scope = Scope::global;
|
||||
@@ -846,7 +875,11 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
*current_step_source);
|
||||
}
|
||||
|
||||
return {std::move(deck), {}};
|
||||
return {
|
||||
std::move(deck),
|
||||
{},
|
||||
input_fingerprint(source_bytes),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
+1677
-175
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,12 @@ void validate_nodal_frame(
|
||||
"Nodal IDs, displacement, and reaction fields must have "
|
||||
"matching sizes.");
|
||||
}
|
||||
if (nodal.origins.size() != nodal.node_ids.size()) {
|
||||
add_error(
|
||||
diagnostics,
|
||||
"results.nodal_origin_size_mismatch",
|
||||
"Nodal IDs and origins must have matching sizes.");
|
||||
}
|
||||
|
||||
std::set<std::int64_t> node_ids;
|
||||
for (const NodeId node_id : nodal.node_ids) {
|
||||
@@ -74,6 +80,84 @@ void validate_nodal_frame(
|
||||
}
|
||||
}
|
||||
|
||||
void validate_beam_section_result(
|
||||
const BeamSectionResult& result,
|
||||
std::vector<Diagnostic>& diagnostics) {
|
||||
if (
|
||||
!std::isfinite(result.xi) ||
|
||||
!is_finite(result.section_strain) ||
|
||||
!is_finite(result.section_force) ||
|
||||
!std::isfinite(result.centroid_sigma_xx) ||
|
||||
!std::ranges::all_of(
|
||||
result.sigma_xx,
|
||||
[](const double value) { return std::isfinite(value); })) {
|
||||
add_error(
|
||||
diagnostics,
|
||||
"results.nonfinite_value",
|
||||
"Beam section result contains a nonfinite value.");
|
||||
}
|
||||
}
|
||||
|
||||
void validate_element_frame(
|
||||
const ElementFrame& element,
|
||||
const NodalFrame& nodal,
|
||||
std::vector<Diagnostic>& diagnostics) {
|
||||
std::set<std::int64_t> nodal_ids;
|
||||
for (const NodeId node_id : nodal.node_ids) {
|
||||
nodal_ids.insert(node_id.value());
|
||||
}
|
||||
|
||||
std::set<std::int64_t> element_ids;
|
||||
for (const BeamElementFrame& beam : element.beams) {
|
||||
if (!element_ids.insert(beam.element.value()).second) {
|
||||
add_error(
|
||||
diagnostics,
|
||||
"results.duplicate_element_id",
|
||||
"Element frame contains duplicate element ID " +
|
||||
std::to_string(beam.element.value()) + ".");
|
||||
}
|
||||
|
||||
if (
|
||||
!is_finite(beam.local_frame.ex) ||
|
||||
!is_finite(beam.local_frame.ey) ||
|
||||
!is_finite(beam.local_frame.ez)) {
|
||||
add_error(
|
||||
diagnostics,
|
||||
"results.nonfinite_value",
|
||||
"Beam local frame contains a nonfinite component.");
|
||||
}
|
||||
|
||||
const BeamSectionResult& first = beam.end_results[0];
|
||||
const BeamSectionResult& second = beam.end_results[1];
|
||||
if (first.end_node == second.end_node) {
|
||||
add_error(
|
||||
diagnostics,
|
||||
"results.duplicate_beam_end_node",
|
||||
"Beam element result contains the same node at both ends.");
|
||||
}
|
||||
if (
|
||||
first.xi != -1.0 || second.xi != 1.0 ||
|
||||
!nodal_ids.contains(first.end_node.value()) ||
|
||||
!nodal_ids.contains(second.end_node.value())) {
|
||||
add_error(
|
||||
diagnostics,
|
||||
"results.invalid_beam_connectivity",
|
||||
"Beam end results must follow (-1, +1) connectivity and "
|
||||
"reference nodes in the nodal frame.");
|
||||
}
|
||||
if (first.sigma_xx.size() != second.sigma_xx.size()) {
|
||||
add_error(
|
||||
diagnostics,
|
||||
"results.recovery_point_count_mismatch",
|
||||
"Beam end results must have matching recovery-point "
|
||||
"counts.");
|
||||
}
|
||||
|
||||
validate_beam_section_result(first, diagnostics);
|
||||
validate_beam_section_result(second, diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status validate_result_database(const ResultDatabase& database) {
|
||||
@@ -106,6 +190,8 @@ Status validate_result_database(const ResultDatabase& database) {
|
||||
}
|
||||
|
||||
validate_nodal_frame(frame.nodal, diagnostics);
|
||||
validate_element_frame(
|
||||
frame.element, frame.nodal, diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+44
-2
@@ -360,6 +360,24 @@ add_test(
|
||||
--gtest_filter=RigidBody.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME BeamRecovery
|
||||
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||
--gtest_filter=BeamRecovery.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME SectionForce
|
||||
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||
--gtest_filter=SectionForce.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME CentroidStress
|
||||
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
|
||||
--gtest_filter=CentroidStress.*
|
||||
)
|
||||
|
||||
add_executable(fesa_serial_assembly_tests
|
||||
unit/assembly/serial_assembler_test.cpp
|
||||
)
|
||||
@@ -580,6 +598,18 @@ add_test(
|
||||
--gtest_filter=ResultDatabase.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ElementFrame
|
||||
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||
--gtest_filter=ElementFrame.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ResultContractMetadata
|
||||
COMMAND "$<TARGET_FILE:fesa_result_database_tests>"
|
||||
--gtest_filter=CompleteResultContract.*
|
||||
)
|
||||
|
||||
add_executable(fesa_hdf5_results_tests
|
||||
integration/io/hdf5_results_test.cpp
|
||||
)
|
||||
@@ -617,8 +647,14 @@ add_test(
|
||||
--gtest_filter=ResultRoundTrip.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME SelfContainedHdf5
|
||||
COMMAND "$<TARGET_FILE:fesa_hdf5_results_tests>"
|
||||
--gtest_filter=SelfContainedHdf5.*
|
||||
)
|
||||
|
||||
set_property(
|
||||
TEST Hdf5 ResultRoundTrip
|
||||
TEST Hdf5 ResultRoundTrip SelfContainedHdf5
|
||||
PROPERTY ENVIRONMENT_MODIFICATION
|
||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||
)
|
||||
@@ -656,8 +692,14 @@ add_test(
|
||||
--gtest_filter=StaticEquilibrium.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME CompleteResultContract
|
||||
COMMAND "$<TARGET_FILE:fesa_linear_static_analysis_tests>"
|
||||
--gtest_filter=CompleteResultContract.*
|
||||
)
|
||||
|
||||
set_property(
|
||||
TEST LinearStaticAnalysis StaticEquilibrium
|
||||
TEST LinearStaticAnalysis StaticEquilibrium CompleteResultContract
|
||||
PROPERTY ENVIRONMENT_MODIFICATION
|
||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||
)
|
||||
|
||||
@@ -98,6 +98,63 @@ void replace_step_time_with_vector(const std::filesystem::path& path) {
|
||||
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
|
||||
}
|
||||
|
||||
void delete_link(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view link_path) {
|
||||
const std::string encoded_path = hdf5_path(path);
|
||||
const std::string owned_link_path{link_path};
|
||||
TestHdf5Handle file{
|
||||
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||
&H5Fclose,
|
||||
};
|
||||
require_hdf5_status(
|
||||
H5Ldelete(file.get(), owned_link_path.c_str(), H5P_DEFAULT));
|
||||
}
|
||||
|
||||
void copy_object(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view source_path,
|
||||
const std::string_view target_path) {
|
||||
const std::string encoded_path = hdf5_path(path);
|
||||
const std::string owned_source_path{source_path};
|
||||
const std::string owned_target_path{target_path};
|
||||
TestHdf5Handle file{
|
||||
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||
&H5Fclose,
|
||||
};
|
||||
require_hdf5_status(H5Ocopy(
|
||||
file.get(),
|
||||
owned_source_path.c_str(),
|
||||
file.get(),
|
||||
owned_target_path.c_str(),
|
||||
H5P_DEFAULT,
|
||||
H5P_DEFAULT));
|
||||
}
|
||||
|
||||
void write_double_attribute(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view object_path,
|
||||
const std::string_view attribute_name,
|
||||
const double value) {
|
||||
const std::string encoded_path = hdf5_path(path);
|
||||
const std::string owned_object_path{object_path};
|
||||
const std::string owned_attribute_name{attribute_name};
|
||||
TestHdf5Handle file{
|
||||
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||
&H5Fclose,
|
||||
};
|
||||
TestHdf5Handle object{
|
||||
H5Oopen(file.get(), owned_object_path.c_str(), H5P_DEFAULT),
|
||||
&H5Oclose,
|
||||
};
|
||||
TestHdf5Handle attribute{
|
||||
H5Aopen(object.get(), owned_attribute_name.c_str(), H5P_DEFAULT),
|
||||
&H5Aclose,
|
||||
};
|
||||
require_hdf5_status(
|
||||
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, &value));
|
||||
}
|
||||
|
||||
void write_int64_dataset(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view dataset_path,
|
||||
@@ -146,12 +203,69 @@ void write_double_dataset(
|
||||
values.data()));
|
||||
}
|
||||
|
||||
void write_uint8_dataset(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view dataset_path,
|
||||
const std::span<const std::uint8_t> values) {
|
||||
const std::string encoded_path = hdf5_path(path);
|
||||
const std::string owned_dataset_path{dataset_path};
|
||||
TestHdf5Handle file{
|
||||
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||
&H5Fclose,
|
||||
};
|
||||
TestHdf5Handle dataset{
|
||||
H5Dopen2(
|
||||
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
|
||||
&H5Dclose,
|
||||
};
|
||||
require_hdf5_status(H5Dwrite(
|
||||
dataset.get(),
|
||||
H5T_NATIVE_UINT8,
|
||||
H5S_ALL,
|
||||
H5S_ALL,
|
||||
H5P_DEFAULT,
|
||||
values.data()));
|
||||
}
|
||||
|
||||
void write_root_string_attribute(
|
||||
const std::filesystem::path& path,
|
||||
const std::string_view name,
|
||||
const std::string_view value) {
|
||||
const std::string encoded_path = hdf5_path(path);
|
||||
const std::string owned_name{name};
|
||||
const std::string owned_value{value};
|
||||
TestHdf5Handle file{
|
||||
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||
&H5Fclose,
|
||||
};
|
||||
TestHdf5Handle attribute{
|
||||
H5Aopen(file.get(), owned_name.c_str(), H5P_DEFAULT),
|
||||
&H5Aclose,
|
||||
};
|
||||
TestHdf5Handle type{H5Aget_type(attribute.get()), &H5Tclose};
|
||||
const char* pointer = owned_value.c_str();
|
||||
require_hdf5_status(H5Awrite(attribute.get(), type.get(), &pointer));
|
||||
}
|
||||
|
||||
std::filesystem::path round_trip_path() {
|
||||
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||
"Temporary" / "fesa-round-trip.h5";
|
||||
}
|
||||
|
||||
fesa::Domain make_domain() {
|
||||
std::filesystem::path self_contained_path() {
|
||||
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||
"Temporary" / "fesa-self-contained.h5";
|
||||
}
|
||||
|
||||
const fesa::Hdf5InputIdentity& test_input_identity() {
|
||||
static const fesa::Hdf5InputIdentity identity{
|
||||
"beam model.inp",
|
||||
"fnv1a64:0123456789abcdef",
|
||||
};
|
||||
return identity;
|
||||
}
|
||||
|
||||
fesa::Domain make_domain(const bool add_unreported_node = false) {
|
||||
fesa::DomainBuilder builder;
|
||||
builder.add_node({
|
||||
fesa::NodeId{42},
|
||||
@@ -161,8 +275,15 @@ fesa::Domain make_domain() {
|
||||
builder.add_node({
|
||||
fesa::NodeId{7},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
|
||||
fesa::Vec3{4.0, 5.5, -6.25},
|
||||
fesa::Vec3{2.25, -2.5, 3.75},
|
||||
});
|
||||
if (add_unreported_node) {
|
||||
builder.add_node({
|
||||
fesa::NodeId{99},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 1003},
|
||||
fesa::Vec3{8.0, 0.0, 0.0},
|
||||
});
|
||||
}
|
||||
builder.add_material({
|
||||
fesa::MaterialId{6},
|
||||
"Steel",
|
||||
@@ -180,7 +301,7 @@ fesa::Domain make_domain() {
|
||||
0.032,
|
||||
fesa::ShearPropertySource::input,
|
||||
fesa::Vec3{0.0, 1.0, 0.0},
|
||||
{},
|
||||
{{0.25, -0.5}, {-0.75, 0.125}},
|
||||
});
|
||||
builder.add_section({
|
||||
fesa::SectionId{12},
|
||||
@@ -209,7 +330,22 @@ fesa::Domain make_domain() {
|
||||
fesa::MaterialId{6},
|
||||
fesa::SectionId{4},
|
||||
});
|
||||
builder.set_step({"Load/Case", {}, {}});
|
||||
builder.add_node_set({"Fixed", {fesa::NodeId{42}}});
|
||||
builder.add_node_set(
|
||||
{"Loaded", {fesa::NodeId{7}, fesa::NodeId{42}}});
|
||||
builder.add_element_set(
|
||||
{"AllBeams", {fesa::ElementId{9}, fesa::ElementId{17}}});
|
||||
builder.set_step({
|
||||
"Load/Case",
|
||||
{
|
||||
{fesa::NodeId{42}, 1, 0.0},
|
||||
{fesa::NodeId{7}, 6, 0.125},
|
||||
},
|
||||
{{
|
||||
fesa::NodeId{7},
|
||||
{100.0, -200.0, 300.0, -400.0, 500.0, -600.0},
|
||||
}},
|
||||
});
|
||||
|
||||
auto built = std::move(builder).build();
|
||||
EXPECT_TRUE(built.domain.has_value());
|
||||
@@ -217,15 +353,26 @@ fesa::Domain make_domain() {
|
||||
return std::move(*built.domain);
|
||||
}
|
||||
|
||||
fesa::BeamSectionResult make_end_result(
|
||||
double xi,
|
||||
fesa::NodeId node,
|
||||
double offset);
|
||||
|
||||
fesa::ResultDatabase make_database() {
|
||||
return {
|
||||
"1.0.0",
|
||||
fesa::ResultDatabase database{
|
||||
"2.0.0",
|
||||
{{
|
||||
"Load/Case",
|
||||
{{
|
||||
1.25,
|
||||
{
|
||||
{fesa::NodeId{7}, fesa::NodeId{42}},
|
||||
{
|
||||
fesa::EntityOrigin{
|
||||
"BeamPart", "Beam-1", 1002},
|
||||
fesa::EntityOrigin{
|
||||
"BeamPart", "Beam-1", 1001},
|
||||
},
|
||||
{
|
||||
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
|
||||
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
|
||||
@@ -236,9 +383,89 @@ fesa::ResultDatabase make_database() {
|
||||
},
|
||||
},
|
||||
{},
|
||||
{},
|
||||
}},
|
||||
}},
|
||||
};
|
||||
database.steps[0].frames[0].element.beams = {
|
||||
{
|
||||
fesa::ElementId{9},
|
||||
{"BeamPart", "Beam-1", 2001},
|
||||
{
|
||||
{-1.0, 0.0, 0.0},
|
||||
{0.0, 1.0, 0.0},
|
||||
{0.0, 0.0, -1.0},
|
||||
},
|
||||
{
|
||||
make_end_result(-1.0, fesa::NodeId{7}, 0.0),
|
||||
make_end_result(1.0, fesa::NodeId{42}, 100.0),
|
||||
},
|
||||
},
|
||||
{
|
||||
fesa::ElementId{17},
|
||||
{"BeamPart", "Beam-1", 2002},
|
||||
{
|
||||
{1.0, 0.0, 0.0},
|
||||
{0.0, 1.0, 0.0},
|
||||
{0.0, 0.0, 1.0},
|
||||
},
|
||||
{
|
||||
make_end_result(-1.0, fesa::NodeId{42}, 200.0),
|
||||
make_end_result(1.0, fesa::NodeId{7}, 300.0),
|
||||
},
|
||||
},
|
||||
};
|
||||
return database;
|
||||
}
|
||||
|
||||
fesa::BeamSectionResult make_end_result(
|
||||
const double xi,
|
||||
const fesa::NodeId node,
|
||||
const double offset) {
|
||||
return {
|
||||
xi,
|
||||
node,
|
||||
{
|
||||
offset + 1.0,
|
||||
offset + 2.0,
|
||||
offset + 3.0,
|
||||
offset + 4.0,
|
||||
offset + 5.0,
|
||||
offset + 6.0,
|
||||
},
|
||||
{
|
||||
offset + 10.0,
|
||||
offset + 20.0,
|
||||
offset + 30.0,
|
||||
offset + 40.0,
|
||||
offset + 50.0,
|
||||
offset + 60.0,
|
||||
},
|
||||
offset + 70.0,
|
||||
{offset + 80.0, offset + 90.0},
|
||||
};
|
||||
}
|
||||
|
||||
fesa::ResultDatabase make_complete_database() {
|
||||
fesa::ResultDatabase database = make_database();
|
||||
auto& frame = database.steps[0].frames[0];
|
||||
frame.diagnostics = {
|
||||
{
|
||||
fesa::DiagnosticStage::solver,
|
||||
fesa::Severity::warning,
|
||||
"solver.residual",
|
||||
"Residual diagnostic",
|
||||
fesa::SourceLocation{"beam model.inp", 41, 7},
|
||||
},
|
||||
{
|
||||
fesa::DiagnosticStage::results,
|
||||
fesa::Severity::error,
|
||||
"results.equilibrium",
|
||||
"Equilibrium diagnostic",
|
||||
std::nullopt,
|
||||
},
|
||||
};
|
||||
return database;
|
||||
}
|
||||
|
||||
bool has_results_error(
|
||||
@@ -260,8 +487,8 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
const auto domain = make_domain();
|
||||
const auto database = make_database();
|
||||
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, domain, database);
|
||||
const auto write_diagnostics = fesa::write_hdf5(
|
||||
path, domain, database, test_input_identity());
|
||||
ASSERT_TRUE(write_diagnostics.empty());
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
@@ -269,7 +496,7 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
ASSERT_TRUE(read.database.has_value());
|
||||
ASSERT_TRUE(read.model.has_value());
|
||||
|
||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
||||
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||
ASSERT_EQ(read.database->steps.size(), 1U);
|
||||
const auto& step = read.database->steps[0];
|
||||
EXPECT_EQ(step.name, "Load/Case");
|
||||
@@ -279,6 +506,12 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
EXPECT_EQ(
|
||||
frame.nodal.node_ids,
|
||||
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
|
||||
EXPECT_EQ(
|
||||
frame.nodal.origins,
|
||||
(std::vector<fesa::EntityOrigin>{
|
||||
{"BeamPart", "Beam-1", 1002},
|
||||
{"BeamPart", "Beam-1", 1001},
|
||||
}));
|
||||
EXPECT_EQ(
|
||||
frame.nodal.displacement[0],
|
||||
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
||||
@@ -308,9 +541,9 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
EXPECT_EQ(
|
||||
read.model->nodes[1].origin,
|
||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 4.0);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, 5.5);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, -6.25);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 2.25);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, -2.5);
|
||||
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, 3.75);
|
||||
|
||||
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
|
||||
@@ -341,6 +574,223 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
fesa::ShearPropertySource::phase1_default);
|
||||
}
|
||||
|
||||
TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
|
||||
const auto path = self_contained_path();
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::filesystem::remove(path);
|
||||
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path,
|
||||
make_domain(),
|
||||
make_complete_database(),
|
||||
test_input_identity())
|
||||
.empty());
|
||||
|
||||
const fesa::Hdf5ReadResult read = fesa::read_hdf5_results(path);
|
||||
ASSERT_TRUE(read.diagnostics.empty());
|
||||
ASSERT_TRUE(read.metadata.has_value());
|
||||
ASSERT_TRUE(read.model.has_value());
|
||||
ASSERT_TRUE(read.analysis.has_value());
|
||||
ASSERT_TRUE(read.database.has_value());
|
||||
|
||||
EXPECT_EQ(read.metadata->schema_version, "2.0.0");
|
||||
EXPECT_EQ(read.metadata->fesa_version, "0.1.0");
|
||||
EXPECT_EQ(
|
||||
read.metadata->unit_policy,
|
||||
"consistent_input_units_no_conversion");
|
||||
EXPECT_EQ(read.metadata->input_source, "beam model.inp");
|
||||
EXPECT_EQ(
|
||||
read.metadata->input_fingerprint,
|
||||
"fnv1a64:0123456789abcdef");
|
||||
|
||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||
ASSERT_EQ(read.model->elements.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
read.model->elements[0].origin,
|
||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 2001}));
|
||||
EXPECT_EQ(read.model->elements[0].material, fesa::MaterialId{6});
|
||||
ASSERT_EQ(read.model->materials.size(), 1U);
|
||||
EXPECT_EQ(read.model->materials[0].name, "Steel");
|
||||
EXPECT_DOUBLE_EQ(read.model->materials[0].young, 210.0e9);
|
||||
EXPECT_DOUBLE_EQ(read.model->materials[0].poisson, 0.3);
|
||||
ASSERT_EQ(read.model->sections.size(), 2U);
|
||||
EXPECT_EQ(read.model->sections[0].name, "InputShear");
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[0].area, 0.04);
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[0].iy, 1.2e-4);
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[0].iz, 1.4e-4);
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[0].torsion_j, 2.0e-4);
|
||||
EXPECT_DOUBLE_EQ(read.model->sections[0].orientation.y, 1.0);
|
||||
EXPECT_EQ(
|
||||
read.model->sections[0].recovery_points,
|
||||
(std::vector<std::array<double, 2>>{
|
||||
{0.25, -0.5}, {-0.75, 0.125}}));
|
||||
ASSERT_EQ(read.model->node_sets.size(), 2U);
|
||||
EXPECT_EQ(read.model->node_sets[0].name, "Fixed");
|
||||
EXPECT_EQ(
|
||||
read.model->node_sets[1].members,
|
||||
(std::vector<fesa::NodeId>{
|
||||
fesa::NodeId{7}, fesa::NodeId{42}}));
|
||||
ASSERT_EQ(read.model->element_sets.size(), 1U);
|
||||
EXPECT_EQ(
|
||||
read.model->element_sets[0].members,
|
||||
(std::vector<fesa::ElementId>{
|
||||
fesa::ElementId{9}, fesa::ElementId{17}}));
|
||||
|
||||
EXPECT_EQ(read.analysis->step.name, "Load/Case");
|
||||
ASSERT_EQ(read.analysis->step.prescribed_dofs.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
read.analysis->step.prescribed_dofs[1].node,
|
||||
fesa::NodeId{7});
|
||||
EXPECT_EQ(read.analysis->step.prescribed_dofs[1].dof, 6U);
|
||||
EXPECT_DOUBLE_EQ(
|
||||
read.analysis->step.prescribed_dofs[1].value, 0.125);
|
||||
ASSERT_EQ(read.analysis->step.nodal_loads.size(), 1U);
|
||||
EXPECT_DOUBLE_EQ(
|
||||
read.analysis->step.nodal_loads[0].values[5], -600.0);
|
||||
EXPECT_EQ(read.analysis->solver.backend, "mkl_pardiso");
|
||||
EXPECT_EQ(
|
||||
read.analysis->solver.constraint_method,
|
||||
"essential_dof_elimination");
|
||||
EXPECT_EQ(read.analysis->solver.assembly, "deterministic_serial");
|
||||
|
||||
const auto& frame = read.database->steps[0].frames[0];
|
||||
ASSERT_EQ(frame.element.beams.size(), 2U);
|
||||
EXPECT_EQ(frame.element.beams[0].element, fesa::ElementId{9});
|
||||
EXPECT_DOUBLE_EQ(frame.element.beams[0].local_frame.ex.x, -1.0);
|
||||
EXPECT_EQ(
|
||||
frame.element.beams[0].end_results[0].end_node,
|
||||
fesa::NodeId{7});
|
||||
EXPECT_EQ(
|
||||
frame.element.beams[0].end_results[1].section_force,
|
||||
(std::array<double, 6>{
|
||||
110.0, 120.0, 130.0, 140.0, 150.0, 160.0}));
|
||||
EXPECT_EQ(
|
||||
frame.element.beams[1].end_results[0].sigma_xx,
|
||||
(std::vector<double>{280.0, 290.0}));
|
||||
ASSERT_EQ(frame.diagnostics.size(), 2U);
|
||||
EXPECT_EQ(frame.diagnostics[0].stage, fesa::DiagnosticStage::solver);
|
||||
EXPECT_EQ(frame.diagnostics[0].severity, fesa::Severity::warning);
|
||||
ASSERT_TRUE(frame.diagnostics[0].source.has_value());
|
||||
EXPECT_EQ(frame.diagnostics[0].source->file, "beam model.inp");
|
||||
EXPECT_EQ(frame.diagnostics[0].source->line, 41U);
|
||||
EXPECT_EQ(frame.diagnostics[0].source->column, 7U);
|
||||
EXPECT_FALSE(frame.diagnostics[1].source.has_value());
|
||||
}
|
||||
|
||||
TEST(SelfContainedHdf5, RejectsMissingResultFrameBeforeWriting) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-missing-result-frame.h5";
|
||||
std::filesystem::remove(path);
|
||||
auto database = make_complete_database();
|
||||
database.steps[0].frames.clear();
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(
|
||||
path, make_domain(), database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.incomplete_result_frame"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(SelfContainedHdf5, RejectsIncompleteNodalCoverageBeforeWriting) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-incomplete-nodal-results.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto database = make_complete_database();
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(
|
||||
path, make_domain(true), database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.incomplete_result_frame"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(SelfContainedHdf5, RejectsIncompleteBeamCoverageBeforeWriting) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-incomplete-beam-results.h5";
|
||||
std::filesystem::remove(path);
|
||||
auto database = make_complete_database();
|
||||
database.steps[0].frames[0].element.beams.pop_back();
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(
|
||||
path, make_domain(), database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.incomplete_result_frame"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(SelfContainedHdf5, RejectsFiniteLocalFrameThatDisagreesWithModel) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-wrong-local-frame.h5";
|
||||
std::filesystem::remove(path);
|
||||
auto database = make_complete_database();
|
||||
database.steps[0].frames[0].element.beams[0].local_frame = {
|
||||
{1.0, 0.0, 0.0},
|
||||
{0.0, 1.0, 0.0},
|
||||
{0.0, 0.0, 1.0},
|
||||
};
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(
|
||||
path, make_domain(), database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.result_element_mismatch"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" / "fesa-schema-1.h5";
|
||||
std::filesystem::remove(path);
|
||||
auto database = make_database();
|
||||
database.schema_version = "1.0.0";
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(
|
||||
path, make_domain(), database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsMissingInputIdentityBeforeWriting) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-missing-input-identity.h5";
|
||||
std::filesystem::remove(path);
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), {"", ""});
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.invalid_input_identity"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsUnlistedMinorSchemaVersion) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" / "fesa-schema-2-1.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
write_root_string_attribute(path, "schema_version", "2.1.0");
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_FALSE(read.metadata.has_value());
|
||||
EXPECT_FALSE(read.analysis.has_value());
|
||||
EXPECT_TRUE(has_results_error(
|
||||
read.diagnostics, "hdf5.unsupported_schema"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" / "fesa-invalid.h5";
|
||||
@@ -349,14 +799,15 @@ TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
||||
auto database = make_database();
|
||||
database.steps[0].frames[0].nodal.reaction.pop_back();
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
const auto diagnostics =
|
||||
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(
|
||||
has_results_error(diagnostics, "results.nodal_size_mismatch"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
||||
TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-unsupported-diagnostics.h5";
|
||||
@@ -371,11 +822,21 @@ TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
||||
std::nullopt,
|
||||
});
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
const auto diagnostics =
|
||||
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.unsupported_result_diagnostics"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
ASSERT_TRUE(diagnostics.empty());
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
ASSERT_TRUE(read.diagnostics.empty());
|
||||
ASSERT_TRUE(read.database.has_value());
|
||||
const auto& stored = read.database->steps[0].frames[0].diagnostics;
|
||||
ASSERT_EQ(stored.size(), 1U);
|
||||
EXPECT_EQ(stored[0].stage, fesa::DiagnosticStage::solver);
|
||||
EXPECT_EQ(stored[0].severity, fesa::Severity::warning);
|
||||
EXPECT_EQ(stored[0].code, "solver.residual");
|
||||
EXPECT_EQ(stored[0].message, "Residual diagnostic");
|
||||
EXPECT_FALSE(stored[0].source.has_value());
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
||||
@@ -385,9 +846,14 @@ TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
||||
std::filesystem::remove(path);
|
||||
const auto domain = make_domain();
|
||||
auto database = make_database();
|
||||
database.steps[0].frames[0].nodal.node_ids[0] = fesa::NodeId{999};
|
||||
auto& nodal = database.steps[0].frames[0].nodal;
|
||||
nodal.node_ids.push_back(fesa::NodeId{999});
|
||||
nodal.origins.push_back({"BeamPart", "Beam-1", 1999});
|
||||
nodal.displacement.push_back({});
|
||||
nodal.reaction.push_back({});
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
const auto diagnostics =
|
||||
fesa::write_hdf5(path, domain, database, test_input_identity());
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.result_node_not_in_model"));
|
||||
@@ -411,8 +877,8 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonscalar-step-time.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, make_domain(), make_database());
|
||||
const auto write_diagnostics = fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity());
|
||||
ASSERT_TRUE(write_diagnostics.empty());
|
||||
replace_step_time_with_vector(path);
|
||||
|
||||
@@ -423,19 +889,88 @@ TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
|
||||
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsSerializedResultWithoutRequiredFrame) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-missing-serialized-frame.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
delete_link(path, "/results/steps/0/frames/0");
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_FALSE(read.metadata.has_value());
|
||||
EXPECT_FALSE(read.analysis.has_value());
|
||||
EXPECT_TRUE(has_results_error(
|
||||
read.diagnostics, "hdf5.incomplete_result_frame"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsSerializedResultWithExtraFrame) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-extra-serialized-frame.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
copy_object(
|
||||
path,
|
||||
"/results/steps/0/frames/0",
|
||||
"/results/steps/0/frames/1");
|
||||
write_double_attribute(
|
||||
path, "/results/steps/0/frames/1", "step_time", 2.5);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_FALSE(read.metadata.has_value());
|
||||
EXPECT_FALSE(read.analysis.has_value());
|
||||
EXPECT_TRUE(has_results_error(
|
||||
read.diagnostics, "hdf5.incomplete_result_frame"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsInvalidSerializedInputFingerprint) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-invalid-input-fingerprint.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
write_root_string_attribute(
|
||||
path, "input_fingerprint", "sha256:not-the-contract");
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_FALSE(read.metadata.has_value());
|
||||
EXPECT_FALSE(read.analysis.has_value());
|
||||
EXPECT_TRUE(has_results_error(
|
||||
read.diagnostics, "hdf5.invalid_input_identity"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-invalid-result-node.h5";
|
||||
std::filesystem::remove(path);
|
||||
const auto write_diagnostics =
|
||||
fesa::write_hdf5(path, make_domain(), make_database());
|
||||
auto database = make_database();
|
||||
auto& nodal = database.steps[0].frames[0].nodal;
|
||||
nodal.node_ids.push_back(fesa::NodeId{99});
|
||||
nodal.origins.push_back({"BeamPart", "Beam-1", 1003});
|
||||
nodal.displacement.push_back({});
|
||||
nodal.reaction.push_back({});
|
||||
const auto write_diagnostics = fesa::write_hdf5(
|
||||
path, make_domain(true), database, test_input_identity());
|
||||
ASSERT_TRUE(write_diagnostics.empty());
|
||||
const std::array<std::int64_t, 2> node_ids{999, 42};
|
||||
write_int64_dataset(
|
||||
path,
|
||||
"/results/steps/0/frames/0/nodal/node_ids",
|
||||
node_ids);
|
||||
const std::array<std::int64_t, 3> node_ids{42, 7, 100};
|
||||
write_int64_dataset(path, "/model/nodes/internal_id", node_ids);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
@@ -450,8 +985,9 @@ TEST(Hdf5, RejectsDuplicateSerializedNodeIds) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-node-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<std::int64_t, 2> ids{42, 42};
|
||||
write_int64_dataset(path, "/model/nodes/internal_id", ids);
|
||||
|
||||
@@ -468,8 +1004,9 @@ TEST(Hdf5, RejectsDuplicateSerializedElementIds) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-element-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<std::int64_t, 2> ids{9, 9};
|
||||
write_int64_dataset(path, "/model/elements/internal_id", ids);
|
||||
|
||||
@@ -486,8 +1023,9 @@ TEST(Hdf5, RejectsDuplicateSerializedSectionIds) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-section-ids.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<std::int64_t, 2> ids{4, 4};
|
||||
write_int64_dataset(path, "/model/sections/internal_id", ids);
|
||||
|
||||
@@ -504,15 +1042,16 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonfinite-coordinates.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 6> coordinates{
|
||||
std::numeric_limits<double>::quiet_NaN(),
|
||||
-2.5,
|
||||
3.75,
|
||||
4.0,
|
||||
5.5,
|
||||
-6.25,
|
||||
2.25,
|
||||
-2.5,
|
||||
3.75,
|
||||
};
|
||||
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||
|
||||
@@ -524,13 +1063,128 @@ TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsSerializedZeroLengthElement) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-zero-length-element.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 6> coordinates{
|
||||
1.25, -2.5, 3.75, 1.25, -2.5, 3.75};
|
||||
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsSerializedOrientationParallelToElement) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-parallel-orientation.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 6> orientations{
|
||||
1.0, 0.0, 0.0, 0.0, 1.0, 0.0};
|
||||
write_double_dataset(
|
||||
path, "/model/sections/orientation", orientations);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsDuplicateSerializedNodeOrigins) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-node-origins.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<std::int64_t, 2> labels{1001, 1001};
|
||||
write_int64_dataset(path, "/model/nodes/local_label", labels);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsDuplicateSerializedBoundaryConditions) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-duplicate-boundary-conditions.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<std::int64_t, 2> node_ids{42, 42};
|
||||
const std::array<std::uint8_t, 2> dofs{1, 1};
|
||||
write_int64_dataset(
|
||||
path,
|
||||
"/analysis/steps/0/boundary_conditions/node_ids",
|
||||
node_ids);
|
||||
write_uint8_dataset(
|
||||
path, "/analysis/steps/0/boundary_conditions/dofs", dofs);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(
|
||||
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsSerializedLocalFrameThatDisagreesWithModel) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-serialized-wrong-local-frame.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 18> local_frames{
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 1.0,
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 1.0,
|
||||
};
|
||||
write_double_dataset(
|
||||
path,
|
||||
"/results/steps/0/frames/0/element/beam/local_frame",
|
||||
local_frames);
|
||||
|
||||
const auto read = fesa::read_hdf5_results(path);
|
||||
|
||||
EXPECT_FALSE(read.database.has_value());
|
||||
EXPECT_FALSE(read.model.has_value());
|
||||
EXPECT_TRUE(has_results_error(
|
||||
read.diagnostics, "hdf5.result_element_mismatch"));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonfinite-shear-area.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 2> shear_areas{
|
||||
std::numeric_limits<double>::infinity(),
|
||||
0.05,
|
||||
@@ -550,8 +1204,9 @@ TEST(Hdf5, RejectsNonpositiveSerializedShearArea) {
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-nonpositive-shear-area.h5";
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_TRUE(
|
||||
fesa::write_hdf5(path, make_domain(), make_database()).empty());
|
||||
ASSERT_TRUE(fesa::write_hdf5(
|
||||
path, make_domain(), make_database(), test_input_identity())
|
||||
.empty());
|
||||
const std::array<double, 2> shear_areas{-0.031, 0.05};
|
||||
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ std::string quote(const std::filesystem::path& path) {
|
||||
return '"' + path.string() + '"';
|
||||
}
|
||||
|
||||
std::string path_utf8(const std::filesystem::path& path) {
|
||||
const std::u8string value = path.u8string();
|
||||
return {reinterpret_cast<const char*>(value.data()), value.size()};
|
||||
}
|
||||
|
||||
std::string read_text(const std::filesystem::path& path) {
|
||||
std::ifstream input{path, std::ios::binary};
|
||||
return {
|
||||
@@ -93,8 +98,15 @@ TEST(MinimalCantileverPipeline, WritesReadableFiniteEquilibratedResults) {
|
||||
fesa::read_hdf5_results(output.path());
|
||||
ASSERT_TRUE(read.database.has_value());
|
||||
ASSERT_TRUE(read.model.has_value());
|
||||
ASSERT_TRUE(read.metadata.has_value());
|
||||
EXPECT_TRUE(read.diagnostics.empty());
|
||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
||||
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||
EXPECT_EQ(
|
||||
read.metadata->input_source,
|
||||
path_utf8(fixture_path("minimal_cantilever.inp")));
|
||||
EXPECT_EQ(
|
||||
read.metadata->input_fingerprint,
|
||||
"fnv1a64:73f31da4615f09b3");
|
||||
|
||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
||||
|
||||
@@ -59,7 +59,7 @@ fesa::Domain build_axial_domain(const bool all_constrained) {
|
||||
2.0,
|
||||
fesa::ShearPropertySource::input,
|
||||
fesa::Vec3{0.0, 1.0, 0.0},
|
||||
{},
|
||||
{{1.0, 2.0}, {-1.0, -2.0}},
|
||||
});
|
||||
builder.add_beam_element({
|
||||
fesa::ElementId{0},
|
||||
@@ -149,7 +149,7 @@ TEST(LinearStaticAnalysis, SolvesHandCalculatedAxialBeamInNodeIdOrder) {
|
||||
ASSERT_TRUE(run.results.has_value());
|
||||
EXPECT_TRUE(run.diagnostics.empty());
|
||||
EXPECT_TRUE(fesa::validate_result_database(*run.results).succeeded);
|
||||
EXPECT_EQ(run.results->schema_version, "1.0.0");
|
||||
EXPECT_EQ(run.results->schema_version, "2.0.0");
|
||||
ASSERT_EQ(run.results->steps.size(), 1);
|
||||
EXPECT_EQ(run.results->steps[0].name, "Load");
|
||||
ASSERT_EQ(run.results->steps[0].frames.size(), 1);
|
||||
@@ -214,4 +214,49 @@ TEST(LinearStaticAnalysis, SolvesAllConstrainedSystemWithoutPardiso) {
|
||||
EXPECT_NEAR(nodal.reaction[1][0], -5.0, 1.0e-12);
|
||||
}
|
||||
|
||||
TEST(CompleteResultContract, PreservesOriginsConnectivityAndBeamRecovery) {
|
||||
const fesa::Domain domain = build_axial_domain(false);
|
||||
|
||||
const fesa::AnalysisRunResult run =
|
||||
fesa::LinearStaticAnalysis{}.run(domain);
|
||||
|
||||
ASSERT_TRUE(run.succeeded);
|
||||
ASSERT_TRUE(run.results.has_value());
|
||||
const fesa::ResultFrame& frame = run.results->steps[0].frames[0];
|
||||
EXPECT_EQ(
|
||||
frame.nodal.origins,
|
||||
(std::vector<fesa::EntityOrigin>{
|
||||
{"BeamPart", "Beam-1", 2},
|
||||
{"BeamPart", "Beam-1", 1},
|
||||
}));
|
||||
|
||||
ASSERT_EQ(frame.element.beams.size(), 1U);
|
||||
const fesa::BeamElementFrame& beam = frame.element.beams[0];
|
||||
EXPECT_EQ(beam.element, fesa::ElementId{0});
|
||||
EXPECT_EQ(
|
||||
beam.origin,
|
||||
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1}));
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ex.x, 1.0);
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ex.y, 0.0);
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ex.z, 0.0);
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ey.x, 0.0);
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ey.y, 1.0);
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ey.z, 0.0);
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ez.x, 0.0);
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ez.y, 0.0);
|
||||
EXPECT_DOUBLE_EQ(beam.local_frame.ez.z, 1.0);
|
||||
EXPECT_EQ(beam.end_results[0].end_node, fesa::NodeId{20});
|
||||
EXPECT_EQ(beam.end_results[1].end_node, fesa::NodeId{4});
|
||||
EXPECT_DOUBLE_EQ(beam.end_results[0].xi, -1.0);
|
||||
EXPECT_DOUBLE_EQ(beam.end_results[1].xi, 1.0);
|
||||
for (const fesa::BeamSectionResult& end : beam.end_results) {
|
||||
EXPECT_NEAR(end.section_strain[0], 0.05, 1.0e-12);
|
||||
EXPECT_NEAR(end.section_force[0], 10.0, 1.0e-12);
|
||||
EXPECT_NEAR(end.centroid_sigma_xx, 5.0, 1.0e-12);
|
||||
ASSERT_EQ(end.sigma_xx.size(), 2U);
|
||||
EXPECT_NEAR(end.sigma_xx[0], 5.0, 1.0e-12);
|
||||
EXPECT_NEAR(end.sigma_xx[1], 5.0, 1.0e-12);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -22,6 +22,7 @@ constexpr double kShearAreaZ = 0.2;
|
||||
fesa::Beam3D2Input make_x_axis_input(const double length) {
|
||||
return {
|
||||
{{{0.0, 0.0, 0.0}, {length, 0.0, 0.0}}},
|
||||
{fesa::NodeId{1}, fesa::NodeId{2}},
|
||||
{fesa::MaterialId{1}, "elastic", kYoung, kPoisson},
|
||||
{
|
||||
fesa::SectionId{1},
|
||||
@@ -369,4 +370,191 @@ TEST(Beam3D2, PreservesGlobalEnergyUnderRigidCoordinateRotation) {
|
||||
4096.0 * std::numeric_limits<double>::epsilon());
|
||||
}
|
||||
|
||||
TEST(BeamRecovery, RecoversPureAxialStrainAndForceAtBothEnds) {
|
||||
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||
std::array<double, 12> displacement{};
|
||||
displacement[6] = 0.4;
|
||||
|
||||
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
EXPECT_DOUBLE_EQ(results[0].xi, -1.0);
|
||||
EXPECT_EQ(results[0].end_node, fesa::NodeId{101});
|
||||
EXPECT_DOUBLE_EQ(results[1].xi, 1.0);
|
||||
EXPECT_EQ(results[1].end_node, fesa::NodeId{202});
|
||||
for (const auto& result : results) {
|
||||
expect_relative_near(result.section_strain[0], 0.2);
|
||||
expect_relative_near(result.section_force[0], 16.8);
|
||||
expect_relative_near(result.centroid_sigma_xx, 42.0);
|
||||
for (std::size_t component = 1; component < 6; ++component) {
|
||||
expect_relative_near(result.section_strain[component], 0.0);
|
||||
expect_relative_near(result.section_force[component], 0.0);
|
||||
}
|
||||
EXPECT_TRUE(result.sigma_xx.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BeamRecovery, RecoversPureTorsionAtBothEnds) {
|
||||
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||
std::array<double, 12> displacement{};
|
||||
displacement[9] = 0.6;
|
||||
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||
|
||||
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
for (const auto& result : results) {
|
||||
expect_relative_near(result.section_strain[3], 0.3);
|
||||
expect_relative_near(
|
||||
result.section_force[3],
|
||||
shear_modulus * kTorsionJ * 0.3);
|
||||
for (const std::size_t component :
|
||||
std::array<std::size_t, 5>{0, 1, 2, 4, 5}) {
|
||||
expect_relative_near(result.section_strain[component], 0.0);
|
||||
expect_relative_near(result.section_force[component], 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BeamRecovery, UsesReducedIntegrationPointForShearAtBothEnds) {
|
||||
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||
std::array<double, 12> displacement{};
|
||||
displacement[7] = 1.2;
|
||||
displacement[5] = 0.2;
|
||||
displacement[11] = 0.6;
|
||||
displacement[8] = -0.4;
|
||||
displacement[4] = 0.1;
|
||||
displacement[10] = 0.5;
|
||||
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||
|
||||
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
for (const auto& result : results) {
|
||||
expect_relative_near(result.section_strain[1], 0.2);
|
||||
expect_relative_near(result.section_strain[2], 0.1);
|
||||
expect_relative_near(
|
||||
result.section_force[1],
|
||||
shear_modulus * kShearAreaY * 0.2);
|
||||
expect_relative_near(
|
||||
result.section_force[2],
|
||||
shear_modulus * kShearAreaZ * 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BeamRecovery, UsesBeamFrameForGlobalDisplacements) {
|
||||
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||
input.coordinates = {{{0.0, 0.0, 0.0}, {0.0, 2.0, 0.0}}};
|
||||
input.section.orientation = {0.0, 0.0, 1.0};
|
||||
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||
std::array<double, 12> displacement{};
|
||||
displacement[7] = 0.4;
|
||||
displacement[10] = 0.6;
|
||||
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
|
||||
|
||||
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
for (const auto& result : results) {
|
||||
expect_relative_near(result.section_strain[0], 0.2);
|
||||
expect_relative_near(result.section_force[0], 16.8);
|
||||
expect_relative_near(result.section_strain[3], 0.3);
|
||||
expect_relative_near(
|
||||
result.section_force[3],
|
||||
shear_modulus * kTorsionJ * 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SectionForce, RecoversPureBendingAboutLocalYAtBothEnds) {
|
||||
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||
std::array<double, 12> displacement{};
|
||||
displacement[4] = -0.4;
|
||||
displacement[10] = 0.4;
|
||||
|
||||
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
for (const auto& result : results) {
|
||||
expect_relative_near(result.section_strain[4], 0.4);
|
||||
expect_relative_near(result.section_force[4], 2.52);
|
||||
for (const std::size_t component :
|
||||
std::array<std::size_t, 5>{0, 1, 2, 3, 5}) {
|
||||
expect_relative_near(result.section_strain[component], 0.0);
|
||||
expect_relative_near(result.section_force[component], 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SectionForce, RecoversPureBendingAboutLocalZAtBothEnds) {
|
||||
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||
std::array<double, 12> displacement{};
|
||||
displacement[5] = 0.25;
|
||||
displacement[11] = -0.25;
|
||||
|
||||
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
for (const auto& result : results) {
|
||||
expect_relative_near(result.section_strain[5], -0.25);
|
||||
expect_relative_near(result.section_force[5], -2.625);
|
||||
for (const std::size_t component :
|
||||
std::array<std::size_t, 5>{0, 1, 2, 3, 4}) {
|
||||
expect_relative_near(result.section_strain[component], 0.0);
|
||||
expect_relative_near(result.section_force[component], 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SectionForce, PreservesBiaxialBendingSignsAtBothEnds) {
|
||||
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||
std::array<double, 12> displacement{};
|
||||
displacement[4] = -0.4;
|
||||
displacement[10] = 0.4;
|
||||
displacement[5] = 0.25;
|
||||
displacement[11] = -0.25;
|
||||
|
||||
const auto results = fesa::recover_beam3d2(input, displacement, {});
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
for (const auto& result : results) {
|
||||
expect_relative_near(result.section_force[4], 2.52);
|
||||
expect_relative_near(result.section_force[5], -2.625);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CentroidStress, UsesAxialStressAndPreservesRecoveryPointOrder) {
|
||||
fesa::Beam3D2Input input = make_x_axis_input(2.0);
|
||||
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
|
||||
std::array<double, 12> displacement{};
|
||||
displacement[6] = 0.2;
|
||||
displacement[4] = -0.2;
|
||||
displacement[10] = 0.2;
|
||||
displacement[5] = 0.3;
|
||||
displacement[11] = -0.3;
|
||||
const std::array<std::array<double, 2>, 3> recovery_points{{
|
||||
{2.0, 3.0},
|
||||
{-1.0, 4.0},
|
||||
{5.0, -2.0},
|
||||
}};
|
||||
|
||||
const auto results = fesa::recover_beam3d2(
|
||||
input,
|
||||
displacement,
|
||||
recovery_points);
|
||||
|
||||
ASSERT_EQ(results.size(), 2U);
|
||||
for (const auto& result : results) {
|
||||
expect_relative_near(result.centroid_sigma_xx, 21.0);
|
||||
ASSERT_EQ(result.sigma_xx.size(), recovery_points.size());
|
||||
expect_relative_near(result.sigma_xx[0], 273.0);
|
||||
expect_relative_near(result.sigma_xx[1], 126.0);
|
||||
expect_relative_near(result.sigma_xx[2], 252.0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -66,6 +66,9 @@ TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
|
||||
|
||||
ASSERT_TRUE(result.deck.has_value());
|
||||
EXPECT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_EQ(
|
||||
result.input_fingerprint,
|
||||
"fnv1a64:73f31da4615f09b3");
|
||||
EXPECT_TRUE(result.deck->parts.empty());
|
||||
EXPECT_FALSE(result.deck->assembly.has_value());
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ std::array<double, 6> zeros() {
|
||||
fesa::ResultDatabase valid_database() {
|
||||
fesa::NodalFrame nodal{
|
||||
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||
{
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 10},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 20},
|
||||
},
|
||||
{
|
||||
zeros(),
|
||||
{1.0, 2.0, 3.0, 0.1, 0.2, 0.3},
|
||||
@@ -26,9 +30,41 @@ fesa::ResultDatabase valid_database() {
|
||||
zeros(),
|
||||
},
|
||||
};
|
||||
fesa::ResultFrame frame{1.0, std::move(nodal), {}};
|
||||
fesa::BeamElementFrame beam{
|
||||
fesa::ElementId{3},
|
||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 30},
|
||||
{
|
||||
fesa::Vec3{1.0, 0.0, 0.0},
|
||||
fesa::Vec3{0.0, 1.0, 0.0},
|
||||
fesa::Vec3{0.0, 0.0, 1.0},
|
||||
},
|
||||
{{
|
||||
{
|
||||
-1.0,
|
||||
fesa::NodeId{0},
|
||||
zeros(),
|
||||
zeros(),
|
||||
0.0,
|
||||
{1.0},
|
||||
},
|
||||
{
|
||||
1.0,
|
||||
fesa::NodeId{1},
|
||||
zeros(),
|
||||
zeros(),
|
||||
0.0,
|
||||
{2.0},
|
||||
},
|
||||
}},
|
||||
};
|
||||
fesa::ResultFrame frame{
|
||||
1.0,
|
||||
std::move(nodal),
|
||||
{{std::move(beam)}},
|
||||
{},
|
||||
};
|
||||
fesa::ResultStep step{"Load", {std::move(frame)}};
|
||||
return {"1.0.0", {std::move(step)}};
|
||||
return {"2.0.0", {std::move(step)}};
|
||||
}
|
||||
|
||||
bool has_diagnostic(
|
||||
@@ -72,6 +108,17 @@ TEST(NodalFrame, RejectsReactionSizeMismatch) {
|
||||
EXPECT_TRUE(has_diagnostic(status, "results.nodal_size_mismatch"));
|
||||
}
|
||||
|
||||
TEST(CompleteResultContract, RejectsNodeOriginSizeMismatch) {
|
||||
auto database = valid_database();
|
||||
database.steps[0].frames[0].nodal.origins.pop_back();
|
||||
|
||||
const auto status = fesa::validate_result_database(database);
|
||||
|
||||
EXPECT_FALSE(status.succeeded);
|
||||
EXPECT_TRUE(has_diagnostic(
|
||||
status, "results.nodal_origin_size_mismatch"));
|
||||
}
|
||||
|
||||
TEST(NodalFrame, RejectsDuplicateNodeId) {
|
||||
auto database = valid_database();
|
||||
database.steps[0].frames[0].nodal.node_ids[1] = fesa::NodeId{0};
|
||||
@@ -135,4 +182,110 @@ TEST(ResultDatabase, RejectsNonfiniteFrameTime) {
|
||||
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
||||
}
|
||||
|
||||
TEST(CompleteResultContract, DeclaresCoordinatesAndComponentOrdering) {
|
||||
EXPECT_EQ(
|
||||
fesa::NodalFrame::coordinate_system,
|
||||
fesa::FieldCoordinateSystem::global);
|
||||
EXPECT_EQ(
|
||||
fesa::NodalFrame::displacement_components,
|
||||
(std::array<std::string_view, 6>{
|
||||
"Ux", "Uy", "Uz", "Rx", "Ry", "Rz"}));
|
||||
EXPECT_EQ(
|
||||
fesa::NodalFrame::reaction_components,
|
||||
(std::array<std::string_view, 6>{
|
||||
"RFx", "RFy", "RFz", "RMx", "RMy", "RMz"}));
|
||||
EXPECT_EQ(
|
||||
fesa::BeamElementFrame::coordinate_system,
|
||||
fesa::FieldCoordinateSystem::element_local);
|
||||
EXPECT_EQ(
|
||||
fesa::BeamElementFrame::section_strain_components,
|
||||
(std::array<std::string_view, 6>{
|
||||
"epsilon", "gamma_y", "gamma_z", "kappa_x", "kappa_y",
|
||||
"kappa_z"}));
|
||||
EXPECT_EQ(
|
||||
fesa::BeamElementFrame::section_force_components,
|
||||
(std::array<std::string_view, 6>{
|
||||
"N", "Vy", "Vz", "T", "My", "Mz"}));
|
||||
EXPECT_EQ(
|
||||
fesa::BeamElementFrame::axial_stress_component,
|
||||
std::string_view{"sigma_xx"});
|
||||
}
|
||||
|
||||
TEST(ElementFrame, AcceptsFiniteConnectedBeamResults) {
|
||||
const auto status = fesa::validate_result_database(valid_database());
|
||||
|
||||
EXPECT_TRUE(status.succeeded);
|
||||
EXPECT_TRUE(status.diagnostics.empty());
|
||||
}
|
||||
|
||||
TEST(ElementFrame, RejectsDuplicateElementId) {
|
||||
auto database = valid_database();
|
||||
auto& beams = database.steps[0].frames[0].element.beams;
|
||||
beams.push_back(beams[0]);
|
||||
|
||||
const auto status = fesa::validate_result_database(database);
|
||||
|
||||
EXPECT_FALSE(status.succeeded);
|
||||
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_element_id"));
|
||||
}
|
||||
|
||||
TEST(ElementFrame, RejectsDuplicateEndNode) {
|
||||
auto database = valid_database();
|
||||
auto& ends = database.steps[0].frames[0]
|
||||
.element.beams[0].end_results;
|
||||
ends[1].end_node = ends[0].end_node;
|
||||
|
||||
const auto status = fesa::validate_result_database(database);
|
||||
|
||||
EXPECT_FALSE(status.succeeded);
|
||||
EXPECT_TRUE(has_diagnostic(status, "results.duplicate_beam_end_node"));
|
||||
}
|
||||
|
||||
TEST(ElementFrame, RejectsWrongEndConnectivity) {
|
||||
auto database = valid_database();
|
||||
database.steps[0].frames[0]
|
||||
.element.beams[0].end_results[0].xi = 1.0;
|
||||
|
||||
const auto status = fesa::validate_result_database(database);
|
||||
|
||||
EXPECT_FALSE(status.succeeded);
|
||||
EXPECT_TRUE(has_diagnostic(status, "results.invalid_beam_connectivity"));
|
||||
}
|
||||
|
||||
TEST(ElementFrame, RejectsEndNodeAbsentFromNodalFrame) {
|
||||
auto database = valid_database();
|
||||
database.steps[0].frames[0]
|
||||
.element.beams[0].end_results[1].end_node = fesa::NodeId{99};
|
||||
|
||||
const auto status = fesa::validate_result_database(database);
|
||||
|
||||
EXPECT_FALSE(status.succeeded);
|
||||
EXPECT_TRUE(has_diagnostic(status, "results.invalid_beam_connectivity"));
|
||||
}
|
||||
|
||||
TEST(ElementFrame, RejectsNonfiniteLocalFrameAndSectionValue) {
|
||||
auto database = valid_database();
|
||||
auto& beam = database.steps[0].frames[0].element.beams[0];
|
||||
beam.local_frame.ey.y = std::numeric_limits<double>::infinity();
|
||||
beam.end_results[1].section_force[4] =
|
||||
std::numeric_limits<double>::quiet_NaN();
|
||||
|
||||
const auto status = fesa::validate_result_database(database);
|
||||
|
||||
EXPECT_FALSE(status.succeeded);
|
||||
EXPECT_TRUE(has_diagnostic(status, "results.nonfinite_value"));
|
||||
}
|
||||
|
||||
TEST(ElementFrame, RejectsMismatchedRecoveryPointCount) {
|
||||
auto database = valid_database();
|
||||
database.steps[0].frames[0]
|
||||
.element.beams[0].end_results[1].sigma_xx.push_back(3.0);
|
||||
|
||||
const auto status = fesa::validate_result_database(database);
|
||||
|
||||
EXPECT_FALSE(status.succeeded);
|
||||
EXPECT_TRUE(has_diagnostic(
|
||||
status, "results.recovery_point_count_mismatch"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user