feat(result-contract-completion): step 2 — self-contained-hdf5
This commit is contained in:
+150
-76
@@ -1,90 +1,164 @@
|
|||||||
# 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의 최소 수직 슬라이스를 정의한다.
|
Schema `2.0.0` is the self-contained Phase 1 result contract. One file contains
|
||||||
파일은 활성 `Domain`의 절점, Beam 연결성, 적용된 전단면적과 그 출처, 그리고 전역
|
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,
|
Schema `1.0.0` was the earlier minimal vertical slice. Version `2.0.0` changes
|
||||||
reference CSV 및 진단 dataset을 저장하지 않는다. 이후 같은 major version에서
|
the required model and result objects, so it is a new major version rather than
|
||||||
dataset을 추가할 수 있지만 아래 required object의 의미, 형상 또는 datatype을
|
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`이다.
|
- Root attributes are variable-length UTF-8 strings:
|
||||||
- 정수 dataset은 명시한 little-endian 고정폭 타입을 사용한다.
|
`schema_version="2.0.0"`, `fesa_version`, and
|
||||||
- 실수 dataset은 IEEE 754 little-endian 64-bit 타입을 사용한다.
|
`unit_policy="consistent_input_units_no_conversion"`.
|
||||||
- 문자열 dataset과 attribute는 UTF-8 variable-length string을 사용한다.
|
- Integer datasets use the stated little-endian fixed-width type. Floating
|
||||||
- `dense_index`는 해당 dataset 행의 0-based index이며 연속적이다.
|
datasets use IEEE 754 little-endian `float64`. Strings are variable-length
|
||||||
- `internal_id`와 결과의 `node_ids`는 FESA semantic model의 nonnegative ID다.
|
UTF-8.
|
||||||
- flat/orphan mesh의 `part_name`과 `instance_name`은 빈 문자열이다.
|
- `dense_index` is a contiguous 0-based row index. Semantic `internal_id`
|
||||||
- 결과의 6개 component 순서는
|
values are nonnegative and are not assumed to be dense or ordered.
|
||||||
`(Ux, Uy, Uz, Rx, Ry, Rz)` 및 `(RFx, RFy, RFz, RMx, RMy, RMz)`다.
|
- Flat/orphan mesh `part_name` and `instance_name` values are empty strings.
|
||||||
- Step과 frame group 이름은 각각 0부터 연속된 decimal index다. 원래 Step 이름은
|
- Ragged arrays use an offset dataset of length `row_count + 1`. Offsets start
|
||||||
Step group의 `name` attribute에 저장한다.
|
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.
|
||||||
|
|
||||||
## 3. Required objects
|
## 3. Required objects
|
||||||
|
|
||||||
```text
|
### 3.1 Model
|
||||||
/
|
|
||||||
├── @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]
|
|
||||||
```
|
|
||||||
|
|
||||||
`model/elements/connectivity`는 `model/nodes/dense_index`를 참조한다. 따라서
|
Let `N`, `E`, `M`, `S`, `NS`, and `ES` be the node, Beam element, material,
|
||||||
`internal_id`가 연속적이거나 Domain 저장 순서와 같다고 가정하지 않는다.
|
section, node-set, and element-set counts. Let `P` be the total number of
|
||||||
`section_id`는 `model/sections/internal_id`를 참조한다.
|
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`) |
|
| 0 | `io` |
|
||||||
| `1` | Phase 1 기본값 `Asy=Asz=5A/6`, `SCF=0` (`phase1_default`) |
|
| 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을 검사한다.
|
- The writer validates `ResultDatabase`, exact schema version, model/result ID
|
||||||
- schema `1.0.0`이 표현하지 않는 non-empty frame diagnostics는 파일을 만들기 전에
|
and provenance joins, Beam connectivity, recovery-point counts, and the
|
||||||
`hdf5.unsupported_result_diagnostics`로 거부한다.
|
single-step name before creating the file.
|
||||||
- required object 생성·쓰기·flush·close 중 HDF5 오류가 발생하면 성공으로 반환하지
|
- Required-object creation, write, flush, and close failures become
|
||||||
않고 `DiagnosticStage::results` 오류로 변환한다.
|
`DiagnosticStage::results` errors. A failed write is never reported as
|
||||||
- reader는 schema version, required object, datatype, rank와 shape를 검사한다.
|
success.
|
||||||
- reader는 model internal ID의 uniqueness, finite coordinates 및 finite positive
|
- The reader validates the exact version, required datatypes, ranks, shapes,
|
||||||
shear area를 검사한다.
|
offsets, finite values, uniqueness, references, field metadata, and result
|
||||||
- writer와 reader는 모든 nodal result ID가 `model/nodes/internal_id`에 존재하는지
|
contracts. It does not return a partial database or partial snapshots.
|
||||||
검사하며, 없는 ID를 성공 결과로 반환하지 않는다.
|
- The public reader returns adapter-owned, read-only metadata, model, and
|
||||||
- reader는 nodal result를 `ResultDatabase`로, model dataset을 HDF5 adapter 전용
|
analysis snapshots plus the semantic `ResultDatabase`. No HDF5 object or
|
||||||
read-only inspection model로 반환한다. `Domain`과 `ResultDatabase`에는 HDF5
|
handle escapes the adapter, and the snapshots contain enough information to
|
||||||
저장 계약을 추가하지 않는다.
|
reconstruct the Phase 1 model and run definition without the source deck.
|
||||||
- malformed 또는 지원하지 않는 파일은 부분 database를 반환하지 않는다.
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fesa/core/diagnostic.hpp>
|
#include <fesa/core/diagnostic.hpp>
|
||||||
@@ -26,27 +27,60 @@ struct Hdf5NodeSnapshot final {
|
|||||||
struct Hdf5ElementSnapshot final {
|
struct Hdf5ElementSnapshot final {
|
||||||
std::uint64_t dense_index;
|
std::uint64_t dense_index;
|
||||||
ElementId id;
|
ElementId id;
|
||||||
|
EntityOrigin origin;
|
||||||
std::array<std::uint64_t, 2> connectivity;
|
std::array<std::uint64_t, 2> connectivity;
|
||||||
|
MaterialId material;
|
||||||
SectionId section;
|
SectionId section;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5SectionSnapshot final {
|
struct Hdf5SectionSnapshot final {
|
||||||
SectionId id;
|
SectionId id;
|
||||||
|
std::string name;
|
||||||
|
double area;
|
||||||
|
double iy;
|
||||||
|
double iz;
|
||||||
|
double torsion_j;
|
||||||
double shear_area_y;
|
double shear_area_y;
|
||||||
double shear_area_z;
|
double shear_area_z;
|
||||||
ShearPropertySource shear_source;
|
ShearPropertySource shear_source;
|
||||||
|
Vec3 orientation;
|
||||||
|
std::vector<std::array<double, 2>> recovery_points;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Hdf5ModelSnapshot final {
|
struct Hdf5ModelSnapshot final {
|
||||||
std::vector<Hdf5NodeSnapshot> nodes;
|
std::vector<Hdf5NodeSnapshot> nodes;
|
||||||
std::vector<Hdf5ElementSnapshot> elements;
|
std::vector<Hdf5ElementSnapshot> elements;
|
||||||
|
std::vector<IsotropicElastic> materials;
|
||||||
std::vector<Hdf5SectionSnapshot> sections;
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
struct Hdf5ReadResult final {
|
||||||
std::optional<ResultDatabase> database;
|
std::optional<ResultDatabase> database;
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
std::optional<Hdf5ModelSnapshot> model;
|
std::optional<Hdf5ModelSnapshot> model;
|
||||||
|
std::optional<Hdf5MetadataSnapshot> metadata;
|
||||||
|
std::optional<Hdf5AnalysisSnapshot> analysis;
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
[[nodiscard]] std::vector<Diagnostic> write_hdf5(
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ AnalysisRunResult LinearStaticAnalysis::run(const Domain& domain) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ResultDatabase database{
|
ResultDatabase database{
|
||||||
"1.0.0",
|
"2.0.0",
|
||||||
{{
|
{{
|
||||||
domain.step().name,
|
domain.step().name,
|
||||||
{{
|
{{
|
||||||
|
|||||||
+1483
-152
File diff suppressed because it is too large
Load Diff
@@ -641,8 +641,14 @@ add_test(
|
|||||||
--gtest_filter=ResultRoundTrip.*
|
--gtest_filter=ResultRoundTrip.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SelfContainedHdf5
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_hdf5_results_tests>"
|
||||||
|
--gtest_filter=SelfContainedHdf5.*
|
||||||
|
)
|
||||||
|
|
||||||
set_property(
|
set_property(
|
||||||
TEST Hdf5 ResultRoundTrip
|
TEST Hdf5 ResultRoundTrip SelfContainedHdf5
|
||||||
PROPERTY ENVIRONMENT_MODIFICATION
|
PROPERTY ENVIRONMENT_MODIFICATION
|
||||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -146,11 +146,36 @@ void write_double_dataset(
|
|||||||
values.data()));
|
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() {
|
std::filesystem::path round_trip_path() {
|
||||||
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||||
"Temporary" / "fesa-round-trip.h5";
|
"Temporary" / "fesa-round-trip.h5";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::filesystem::path self_contained_path() {
|
||||||
|
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
|
||||||
|
"Temporary" / "fesa-self-contained.h5";
|
||||||
|
}
|
||||||
|
|
||||||
fesa::Domain make_domain() {
|
fesa::Domain make_domain() {
|
||||||
fesa::DomainBuilder builder;
|
fesa::DomainBuilder builder;
|
||||||
builder.add_node({
|
builder.add_node({
|
||||||
@@ -180,7 +205,7 @@ fesa::Domain make_domain() {
|
|||||||
0.032,
|
0.032,
|
||||||
fesa::ShearPropertySource::input,
|
fesa::ShearPropertySource::input,
|
||||||
fesa::Vec3{0.0, 1.0, 0.0},
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
{},
|
{{0.25, -0.5}, {-0.75, 0.125}},
|
||||||
});
|
});
|
||||||
builder.add_section({
|
builder.add_section({
|
||||||
fesa::SectionId{12},
|
fesa::SectionId{12},
|
||||||
@@ -209,7 +234,22 @@ fesa::Domain make_domain() {
|
|||||||
fesa::MaterialId{6},
|
fesa::MaterialId{6},
|
||||||
fesa::SectionId{4},
|
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();
|
auto built = std::move(builder).build();
|
||||||
EXPECT_TRUE(built.domain.has_value());
|
EXPECT_TRUE(built.domain.has_value());
|
||||||
@@ -219,7 +259,7 @@ fesa::Domain make_domain() {
|
|||||||
|
|
||||||
fesa::ResultDatabase make_database() {
|
fesa::ResultDatabase make_database() {
|
||||||
return {
|
return {
|
||||||
"1.0.0",
|
"2.0.0",
|
||||||
{{
|
{{
|
||||||
"Load/Case",
|
"Load/Case",
|
||||||
{{
|
{{
|
||||||
@@ -248,6 +288,84 @@ fesa::ResultDatabase make_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.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),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
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(
|
bool has_results_error(
|
||||||
const std::vector<fesa::Diagnostic>& diagnostics,
|
const std::vector<fesa::Diagnostic>& diagnostics,
|
||||||
const std::string_view code) {
|
const std::string_view code) {
|
||||||
@@ -276,7 +394,7 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
ASSERT_TRUE(read.database.has_value());
|
ASSERT_TRUE(read.database.has_value());
|
||||||
ASSERT_TRUE(read.model.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);
|
ASSERT_EQ(read.database->steps.size(), 1U);
|
||||||
const auto& step = read.database->steps[0];
|
const auto& step = read.database->steps[0];
|
||||||
EXPECT_EQ(step.name, "Load/Case");
|
EXPECT_EQ(step.name, "Load/Case");
|
||||||
@@ -354,6 +472,134 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
|||||||
fesa::ShearPropertySource::phase1_default);
|
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())
|
||||||
|
.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");
|
||||||
|
|
||||||
|
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(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);
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema"));
|
||||||
|
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()).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) {
|
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" / "fesa-invalid.h5";
|
"Testing" / "Temporary" / "fesa-invalid.h5";
|
||||||
@@ -369,7 +615,7 @@ TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
|||||||
EXPECT_FALSE(std::filesystem::exists(path));
|
EXPECT_FALSE(std::filesystem::exists(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) {
|
||||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||||
"Testing" / "Temporary" /
|
"Testing" / "Temporary" /
|
||||||
"fesa-unsupported-diagnostics.h5";
|
"fesa-unsupported-diagnostics.h5";
|
||||||
@@ -386,9 +632,18 @@ TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
|||||||
|
|
||||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||||
|
|
||||||
EXPECT_TRUE(has_results_error(
|
ASSERT_TRUE(diagnostics.empty());
|
||||||
diagnostics, "hdf5.unsupported_result_diagnostics"));
|
|
||||||
EXPECT_FALSE(std::filesystem::exists(path));
|
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) {
|
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ TEST(MinimalCantileverPipeline, WritesReadableFiniteEquilibratedResults) {
|
|||||||
ASSERT_TRUE(read.database.has_value());
|
ASSERT_TRUE(read.database.has_value());
|
||||||
ASSERT_TRUE(read.model.has_value());
|
ASSERT_TRUE(read.model.has_value());
|
||||||
EXPECT_TRUE(read.diagnostics.empty());
|
EXPECT_TRUE(read.diagnostics.empty());
|
||||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
EXPECT_EQ(read.database->schema_version, "2.0.0");
|
||||||
|
|
||||||
ASSERT_EQ(read.model->nodes.size(), 2U);
|
ASSERT_EQ(read.model->nodes.size(), 2U);
|
||||||
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ TEST(LinearStaticAnalysis, SolvesHandCalculatedAxialBeamInNodeIdOrder) {
|
|||||||
ASSERT_TRUE(run.results.has_value());
|
ASSERT_TRUE(run.results.has_value());
|
||||||
EXPECT_TRUE(run.diagnostics.empty());
|
EXPECT_TRUE(run.diagnostics.empty());
|
||||||
EXPECT_TRUE(fesa::validate_result_database(*run.results).succeeded);
|
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);
|
ASSERT_EQ(run.results->steps.size(), 1);
|
||||||
EXPECT_EQ(run.results->steps[0].name, "Load");
|
EXPECT_EQ(run.results->steps[0].name, "Load");
|
||||||
ASSERT_EQ(run.results->steps[0].frames.size(), 1);
|
ASSERT_EQ(run.results->steps[0].frames.size(), 1);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ fesa::ResultDatabase valid_database() {
|
|||||||
{},
|
{},
|
||||||
};
|
};
|
||||||
fesa::ResultStep step{"Load", {std::move(frame)}};
|
fesa::ResultStep step{"Load", {std::move(frame)}};
|
||||||
return {"1.0.0", {std::move(step)}};
|
return {"2.0.0", {std::move(step)}};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool has_diagnostic(
|
bool has_diagnostic(
|
||||||
|
|||||||
Reference in New Issue
Block a user