fix: strengthen validation diagnostics
This commit is contained in:
@@ -13,7 +13,7 @@ Every new agent session must read this file together with `PROGRESS.md` before p
|
|||||||
- If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
|
- If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
|
||||||
|
|
||||||
## Current Objective
|
## Current Objective
|
||||||
Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-04 validation and singular diagnostic coverage. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset.
|
Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-05 six-DOF DofManager and reaction-foundation coverage. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset.
|
||||||
|
|
||||||
## Required Reading For New Agents
|
## Required Reading For New Agents
|
||||||
1. `AGENTS.md`
|
1. `AGENTS.md`
|
||||||
@@ -36,7 +36,7 @@ Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebase
|
|||||||
## Active Phase Files
|
## Active Phase Files
|
||||||
- Active phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
- Active phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
||||||
- Execute with: `python scripts/execute.py 1-linear-static-mitc4-rebaseline`
|
- Execute with: `python scripts/execute.py 1-linear-static-mitc4-rebaseline`
|
||||||
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step15.md` is the independent evaluator closeout.
|
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step4.md` is complete and strengthened validation/singular diagnostics; `step15.md` is the independent evaluator closeout.
|
||||||
- Every step file contains a sprint contract with objective, required reading, scope, allowed files, explicit non-goals, tests to write first, reference artifacts, acceptance command, evaluator checklist, and handoff requirements.
|
- Every step file contains a sprint contract with objective, required reading, scope, allowed files, explicit non-goals, tests to write first, reference artifacts, acceptance command, evaluator checklist, and handoff requirements.
|
||||||
- Historical phase directory: `phases/1-linear-static-mitc4`
|
- Historical phase directory: `phases/1-linear-static-mitc4`
|
||||||
- Historical phase status: blocked/superseded. Do not resume the old P1-15/P1-16 path unless the user explicitly requests recovery of that exact phase.
|
- Historical phase status: blocked/superseded. Do not resume the old P1-15/P1-16 path unless the user explicitly requests recovery of that exact phase.
|
||||||
@@ -74,7 +74,7 @@ Each gate should be satisfied before moving to the next implementation band unle
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| G0 - Planning readiness | partial | Readiness task R-011 is resolved by `quad_02_phase1.inp`; R-010 and R-013 remain open. | Updated docs, PLAN.md, PROGRESS.md |
|
| G0 - Planning readiness | partial | Readiness task R-011 is resolved by `quad_02_phase1.inp`; R-010 and R-013 remain open. | Updated docs, PLAN.md, PROGRESS.md |
|
||||||
| G1 - Build and validation | satisfied | Build system, test framework, and `scripts/validate_workspace.py` run real checks. | Validation command output |
|
| G1 - Build and validation | satisfied | Build system, test framework, and `scripts/validate_workspace.py` run real checks. | Validation command output |
|
||||||
| G2 - Parser and domain | partial | Parser subset revalidated in step 3; validation and singular diagnostics remain for step 4. | Parser acceptance/rejection tests and validation output |
|
| G2 - Parser and domain | satisfied | Parser subset revalidated in step 3; validation and singular diagnostics revalidated in step 4. | Parser acceptance/rejection tests, validation negative tests, and validation output |
|
||||||
| G3 - DOF/math/results infrastructure | partial | Core aliases, DOF mapping, validation harness, and model diagnostic context were revalidated in step 2; DofManager/results/assembly remain for steps 5, 6, and 12. | P1R-02 validation output |
|
| G3 - DOF/math/results infrastructure | partial | Core aliases, DOF mapping, validation harness, and model diagnostic context were revalidated in step 2; DofManager/results/assembly remain for steps 5, 6, and 12. | P1R-02 validation output |
|
||||||
| G4 - MITC4 element readiness | reopened | MITC4 formulation was rewritten from local papers; element implementation must be rebuilt or revalidated through steps 7 through 11. | Revised `docs/MITC4_FORMULATION.md`, future element tests |
|
| G4 - MITC4 element readiness | reopened | MITC4 formulation was rewritten from local papers; element implementation must be rebuilt or revalidated through steps 7 through 11. | Revised `docs/MITC4_FORMULATION.md`, future element tests |
|
||||||
| G5 - End-to-end solver | reopened | Linear static path must be revalidated through steps 13 and 14 after the MITC4 rebuild and `quad_02` compatibility path. | Future integration/reference regression output |
|
| G5 - End-to-end solver | reopened | Linear static path must be revalidated through steps 13 and 14 after the MITC4 rebuild and `quad_02` compatibility path. | Future integration/reference regression output |
|
||||||
@@ -85,7 +85,7 @@ All milestones are intended to become one or more self-contained sprint contract
|
|||||||
| ID | Status | Owner | Objective | Depends On | Acceptance Focus |
|
| ID | Status | Owner | Objective | Depends On | Acceptance Focus |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| P1R-03 | completed | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | none | Supported keywords accepted; unsupported features rejected |
|
| P1R-03 | completed | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | none | Supported keywords accepted; unsupported features rejected |
|
||||||
| P1R-04 | pending | validation generator | Rebuild validation and singular diagnostic coverage. | P1R-03 | Missing-reference and singular-prone negative tests |
|
| P1R-04 | completed | validation generator | Rebuild validation and singular diagnostic coverage. | P1R-03 | Missing-reference and singular-prone negative tests |
|
||||||
| P1R-05 | pending | DOF generator | Rebuild six-DOF DofManager, constrained/free mapping, equation numbering, and full-vector reconstruction. | none | DOF mapping and reaction foundation tests |
|
| P1R-05 | pending | DOF generator | Rebuild six-DOF DofManager, constrained/free mapping, equation numbering, and full-vector reconstruction. | none | DOF mapping and reaction foundation tests |
|
||||||
| P1R-06 | pending | results generator | Rebuild minimum results model and displacement CSV comparator. | none | U/RF schema tests and CSV comparator tests |
|
| P1R-06 | pending | results generator | Rebuild minimum results model and displacement CSV comparator. | none | U/RF schema tests and CSV comparator tests |
|
||||||
| P1R-07 | pending | MITC4 generator | Implement MITC4 geometry, node order, tying points, directors, and local bases. | none | Shape/basis/diagnostic tests |
|
| P1R-07 | pending | MITC4 generator | Implement MITC4 geometry, node order, tying points, directors, and local bases. | none | Shape/basis/diagnostic tests |
|
||||||
|
|||||||
+26
-1
@@ -13,10 +13,35 @@ Every new agent session must read this file together with `PLAN.md` before plann
|
|||||||
- Do not remove history unless the user explicitly asks for archival cleanup.
|
- Do not remove history unless the user explicitly asks for archival cleanup.
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 3 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, and the Phase 1 parser/domain subset have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 4 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, the Phase 1 parser/domain subset, and validation/singular diagnostics have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
||||||
|
|
||||||
## Completed Work
|
## Completed Work
|
||||||
|
|
||||||
|
### 2026-05-04 - P1R-04 validation and singular diagnostics completed
|
||||||
|
Author: Codex
|
||||||
|
|
||||||
|
Changed files:
|
||||||
|
- `include/fesa/fesa.hpp`
|
||||||
|
- `tests/test_main.cpp`
|
||||||
|
- `phases/1-linear-static-mitc4-rebaseline/index.json`
|
||||||
|
- `PLAN.md`
|
||||||
|
- `PROGRESS.md`
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
- Added negative validation tests for missing node/property/material references, missing shell-section/boundary/load sets, missing set members, non-positive shell thickness, invalid boundary/load DOFs, no active elements, no free DOFs, isolated free DOFs, and weak drilling DOF smoke coverage.
|
||||||
|
- Strengthened `validateDomain` so it reports missing `Nset` node members, missing `Elset` element members, non-positive or non-finite shell thickness, invalid Abaqus DOF ranges, no-free-DOF states, free DOFs untouched by active element connectivity, and free drilling DOF weak-stabilization warnings.
|
||||||
|
- Kept mesh quality diagnostics out of Phase 1; no aspect ratio, skew, warpage, or distortion diagnostics were added.
|
||||||
|
- Added keyword context to solver size, solver singularity, and fallback no-free-DOF diagnostics.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
- First ran `python scripts/validate_workspace.py` after adding tests; it failed as expected on the missing new diagnostics.
|
||||||
|
- After implementing the validation diagnostics, `python scripts/validate_workspace.py` configured CMake, built `fesa_core` and `fesa_tests`, and ran CTest successfully.
|
||||||
|
- CTest result: 1 test executable passed.
|
||||||
|
|
||||||
|
Follow-up:
|
||||||
|
- Continue with P1R-05 DofManager and reaction-foundation revalidation.
|
||||||
|
- Keep RF reference CSV availability open; current Phase 1 RF validation remains internal full-vector equilibrium until a stored `*_reactions.csv` artifact is provided.
|
||||||
|
|
||||||
### 2026-05-04 - P1R-03 parser/domain subset completed
|
### 2026-05-04 - P1R-03 parser/domain subset completed
|
||||||
Author: Codex
|
Author: Codex
|
||||||
|
|
||||||
|
|||||||
+114
-3
@@ -776,6 +776,18 @@ inline const ShellSection* shellSectionForElement(const Domain& domain, GlobalId
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::string dofNameOrNumber(int abaqus_dof) {
|
||||||
|
auto dof = dofFromAbaqus(abaqus_dof);
|
||||||
|
if (dof) {
|
||||||
|
return dofLabel(*dof);
|
||||||
|
}
|
||||||
|
return "DOF " + std::to_string(abaqus_dof);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool validAbaqusDofRange(int first, int last) {
|
||||||
|
return dofFromAbaqus(first).has_value() && dofFromAbaqus(last).has_value() && first <= last;
|
||||||
|
}
|
||||||
|
|
||||||
inline std::vector<Diagnostic> validateDomain(const Domain& domain) {
|
inline std::vector<Diagnostic> validateDomain(const Domain& domain) {
|
||||||
std::vector<Diagnostic> diagnostics;
|
std::vector<Diagnostic> diagnostics;
|
||||||
if (domain.elements.empty()) {
|
if (domain.elements.empty()) {
|
||||||
@@ -785,6 +797,26 @@ inline std::vector<Diagnostic> validateDomain(const Domain& domain) {
|
|||||||
if (domain.boundary_conditions.empty()) {
|
if (domain.boundary_conditions.empty()) {
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-BOUNDARY", "No boundary constraints are defined", "boundary"));
|
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-BOUNDARY", "No boundary constraints are defined", "boundary"));
|
||||||
}
|
}
|
||||||
|
for (const auto& [set_key, set] : domain.node_sets) {
|
||||||
|
(void)set_key;
|
||||||
|
for (GlobalId node_id : set.node_ids) {
|
||||||
|
if (domain.nodes.count(node_id) == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-NSET-MISSING-NODE",
|
||||||
|
"Node set " + set.name + " references missing node " + std::to_string(node_id),
|
||||||
|
"nset"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& [set_key, set] : domain.element_sets) {
|
||||||
|
(void)set_key;
|
||||||
|
for (GlobalId element_id : set.element_ids) {
|
||||||
|
if (domain.elements.count(element_id) == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-ELSET-MISSING-ELEMENT",
|
||||||
|
"Element set " + set.name + " references missing element " + std::to_string(element_id),
|
||||||
|
"elset"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const auto& [id, element] : domain.elements) {
|
for (const auto& [id, element] : domain.elements) {
|
||||||
for (GlobalId node_id : element.node_ids) {
|
for (GlobalId node_id : element.node_ids) {
|
||||||
if (domain.nodes.count(node_id) == 0) {
|
if (domain.nodes.count(node_id) == 0) {
|
||||||
@@ -800,6 +832,11 @@ inline std::vector<Diagnostic> validateDomain(const Domain& domain) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const ShellSection& section : domain.shell_sections) {
|
for (const ShellSection& section : domain.shell_sections) {
|
||||||
|
if (section.thickness <= 0.0 || !std::isfinite(section.thickness)) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-NONPOSITIVE-THICKNESS",
|
||||||
|
"Shell section for element set " + section.element_set + " has non-positive thickness",
|
||||||
|
"shell section"));
|
||||||
|
}
|
||||||
if (domain.element_sets.count(Domain::key(section.element_set)) == 0) {
|
if (domain.element_sets.count(Domain::key(section.element_set)) == 0) {
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-ELSET",
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-ELSET",
|
||||||
"Shell section references missing element set: " + section.element_set, "shell section"));
|
"Shell section references missing element set: " + section.element_set, "shell section"));
|
||||||
@@ -814,9 +851,19 @@ inline std::vector<Diagnostic> validateDomain(const Domain& domain) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
||||||
|
if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-BOUNDARY-DOF",
|
||||||
|
"Boundary target " + boundary.target + " has invalid DOF range " +
|
||||||
|
dofNameOrNumber(boundary.first_dof) + " to " + dofNameOrNumber(boundary.last_dof),
|
||||||
|
"boundary"));
|
||||||
|
}
|
||||||
(void)resolveNodeTarget(domain, boundary.target, &diagnostics, "boundary");
|
(void)resolveNodeTarget(domain, boundary.target, &diagnostics, "boundary");
|
||||||
}
|
}
|
||||||
for (const NodalLoad& load : domain.loads) {
|
for (const NodalLoad& load : domain.loads) {
|
||||||
|
if (!dofFromAbaqus(load.dof)) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-CLOAD-DOF",
|
||||||
|
"Load target " + load.target + " has invalid " + dofNameOrNumber(load.dof), "cload"));
|
||||||
|
}
|
||||||
(void)resolveNodeTarget(domain, load.target, &diagnostics, "cload");
|
(void)resolveNodeTarget(domain, load.target, &diagnostics, "cload");
|
||||||
}
|
}
|
||||||
const bool any_nonzero_load = std::any_of(domain.loads.begin(), domain.loads.end(), [](const NodalLoad& load) {
|
const bool any_nonzero_load = std::any_of(domain.loads.begin(), domain.loads.end(), [](const NodalLoad& load) {
|
||||||
@@ -825,6 +872,68 @@ inline std::vector<Diagnostic> validateDomain(const Domain& domain) {
|
|||||||
if (!any_nonzero_load) {
|
if (!any_nonzero_load) {
|
||||||
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-NONZERO-LOAD", "No nonzero load is defined", "cload"));
|
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-NONZERO-LOAD", "No nonzero load is defined", "cload"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::set<std::pair<GlobalId, int>> constrained_dofs;
|
||||||
|
for (const BoundaryCondition& boundary : domain.boundary_conditions) {
|
||||||
|
if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (GlobalId node_id : resolveNodeTarget(domain, boundary.target)) {
|
||||||
|
if (domain.nodes.count(node_id) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) {
|
||||||
|
constrained_dofs.insert(std::make_pair(node_id, dof - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<GlobalId> active_connectivity_nodes;
|
||||||
|
for (const auto& [element_id, element] : domain.elements) {
|
||||||
|
(void)element_id;
|
||||||
|
for (GlobalId node_id : element.node_ids) {
|
||||||
|
if (domain.nodes.count(node_id) != 0) {
|
||||||
|
active_connectivity_nodes.insert(node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalIndex free_dof_count = 0;
|
||||||
|
LocalIndex weak_drilling_count = 0;
|
||||||
|
GlobalId weak_drilling_example = 0;
|
||||||
|
for (const auto& [node_id, node] : domain.nodes) {
|
||||||
|
(void)node;
|
||||||
|
for (Dof dof : allDofs()) {
|
||||||
|
const auto key = std::make_pair(node_id, dofIndex(dof));
|
||||||
|
if (constrained_dofs.count(key) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++free_dof_count;
|
||||||
|
if (!domain.elements.empty() && active_connectivity_nodes.count(node_id) == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-DOF-UNTOUCHED",
|
||||||
|
"Node " + std::to_string(node_id) + " DOF " + dofLabel(dof) +
|
||||||
|
" is free but is not touched by active element connectivity",
|
||||||
|
"dof"));
|
||||||
|
}
|
||||||
|
if (!domain.elements.empty() && active_connectivity_nodes.count(node_id) != 0 && dof == Dof::RZ) {
|
||||||
|
if (weak_drilling_count == 0) {
|
||||||
|
weak_drilling_example = node_id;
|
||||||
|
}
|
||||||
|
++weak_drilling_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!domain.nodes.empty() && free_dof_count == 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-FREE-DOFS",
|
||||||
|
"No free DOFs exist after applying boundary constraints", "dof"));
|
||||||
|
}
|
||||||
|
if (weak_drilling_count > 0) {
|
||||||
|
diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-WEAK-DRILLING-DOF",
|
||||||
|
"Node " + std::to_string(weak_drilling_example) +
|
||||||
|
" DOF RZ is free; drilling rotation is weakly stabilized in Phase 1 (" +
|
||||||
|
std::to_string(weak_drilling_count) + " free drilling DOF(s))",
|
||||||
|
"dof"));
|
||||||
|
}
|
||||||
return diagnostics;
|
return diagnostics;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -968,7 +1077,7 @@ class GaussianEliminationSolver final : public LinearSolver {
|
|||||||
const LocalIndex n = a.rows();
|
const LocalIndex n = a.rows();
|
||||||
SolveResult result;
|
SolveResult result;
|
||||||
if (a.rows() != a.cols() || static_cast<LocalIndex>(b.size()) != n) {
|
if (a.rows() != a.cols() || static_cast<LocalIndex>(b.size()) != n) {
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-SOLVER-SIZE", "Linear system size mismatch", {}});
|
result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SOLVER-SIZE", "Linear system size mismatch", "solver"));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
for (LocalIndex col = 0; col < n; ++col) {
|
for (LocalIndex col = 0; col < n; ++col) {
|
||||||
@@ -982,7 +1091,8 @@ class GaussianEliminationSolver final : public LinearSolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pivot_abs < 1.0e-12) {
|
if (pivot_abs < 1.0e-12) {
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-SINGULAR-SOLVER", "Reduced system is singular or ill-conditioned", {}});
|
result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-SOLVER",
|
||||||
|
"Reduced system is singular or ill-conditioned", "solver"));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if (pivot != col) {
|
if (pivot != col) {
|
||||||
@@ -1409,7 +1519,8 @@ class LinearStaticAnalysis final : public Analysis {
|
|||||||
void solve(const Domain& domain, AnalysisResult& result) const override {
|
void solve(const Domain& domain, AnalysisResult& result) const override {
|
||||||
DofManager dofs(domain);
|
DofManager dofs(domain);
|
||||||
if (dofs.freeDofCount() == 0) {
|
if (dofs.freeDofCount() == 0) {
|
||||||
result.diagnostics.push_back({Severity::Error, "FESA-SINGULAR-NO-FREE-DOFS", "No free DOFs exist after applying constraints", {}});
|
result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-FREE-DOFS",
|
||||||
|
"No free DOFs exist after applying constraints", "dof"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AssemblyResult assembly = assembleSystem(domain, dofs);
|
AssemblyResult assembly = assembleSystem(domain, dofs);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{ "step": 1, "name": "reference-onboarding", "status": "completed" },
|
{ "step": 1, "name": "reference-onboarding", "status": "completed" },
|
||||||
{ "step": 2, "name": "core-harness-guardrails", "status": "completed" },
|
{ "step": 2, "name": "core-harness-guardrails", "status": "completed" },
|
||||||
{ "step": 3, "name": "parser-domain-subset", "status": "completed" },
|
{ "step": 3, "name": "parser-domain-subset", "status": "completed" },
|
||||||
{ "step": 4, "name": "validation-singular-diagnostics", "status": "pending" },
|
{ "step": 4, "name": "validation-singular-diagnostics", "status": "completed" },
|
||||||
{ "step": 5, "name": "dof-manager-reaction-foundation", "status": "pending" },
|
{ "step": 5, "name": "dof-manager-reaction-foundation", "status": "pending" },
|
||||||
{ "step": 6, "name": "results-comparator-foundation", "status": "pending" },
|
{ "step": 6, "name": "results-comparator-foundation", "status": "pending" },
|
||||||
{ "step": 7, "name": "mitc4-geometry-directors", "status": "pending" },
|
{ "step": 7, "name": "mitc4-geometry-directors", "status": "pending" },
|
||||||
|
|||||||
@@ -124,6 +124,21 @@ std::size_t diagnosticCount(const std::vector<fesa::Diagnostic>& diagnostics, co
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fesa::Domain singleElementValidationDomain() {
|
||||||
|
fesa::Domain domain;
|
||||||
|
domain.nodes[1] = {1, {0, 0, 0}};
|
||||||
|
domain.nodes[2] = {2, {1, 0, 0}};
|
||||||
|
domain.nodes[3] = {3, {1, 1, 0}};
|
||||||
|
domain.nodes[4] = {4, {0, 1, 0}};
|
||||||
|
domain.elements[1] = {1, fesa::ElementType::MITC4, {1, 2, 3, 4}, "EALL"};
|
||||||
|
domain.element_sets["eall"] = {"EALL", {1}};
|
||||||
|
domain.node_sets["all_nodes"] = {"ALL_NODES", {1, 2, 3, 4}};
|
||||||
|
domain.materials["mat"] = {"MAT", 1000.0, 0.3};
|
||||||
|
domain.shell_sections.push_back({"EALL", "MAT", 0.1});
|
||||||
|
domain.loads.push_back({"2", 3, -1.0});
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
FESA_TEST(core_types_and_dof_mapping_are_stable) {
|
FESA_TEST(core_types_and_dof_mapping_are_stable) {
|
||||||
@@ -377,6 +392,102 @@ FESA_TEST(domain_validation_reports_missing_property_and_targets) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FESA_TEST(domain_validation_reports_missing_sets_and_set_members) {
|
||||||
|
auto domain = singleElementValidationDomain();
|
||||||
|
domain.shell_sections.clear();
|
||||||
|
domain.shell_sections.push_back({"MISSING_ELSET", "MISSING_MAT", 0.1});
|
||||||
|
domain.node_sets["bad_nodes"] = {"BAD_NODES", {1, 99}};
|
||||||
|
domain.element_sets["bad_elements"] = {"BAD_ELEMENTS", {1, 77}};
|
||||||
|
domain.boundary_conditions.push_back({"MISSING_BOUNDARY_SET", 1, 6, 0.0});
|
||||||
|
domain.boundary_conditions.push_back({"BAD_NODES", 1, 1, 0.0});
|
||||||
|
domain.loads.push_back({"MISSING_LOAD_SET", 3, 1.0});
|
||||||
|
domain.loads.push_back({"BAD_NODES", 3, 1.0});
|
||||||
|
|
||||||
|
auto diagnostics = fesa::validateDomain(domain);
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-ELSET"));
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-MATERIAL"));
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-NSET"));
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-NSET-MISSING-NODE"));
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-ELSET-MISSING-ELEMENT"));
|
||||||
|
|
||||||
|
const auto* missing_set = findDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-NSET");
|
||||||
|
FESA_CHECK(missing_set != nullptr);
|
||||||
|
FESA_CHECK(missing_set->message.find("MISSING_BOUNDARY_SET") != std::string::npos ||
|
||||||
|
missing_set->message.find("MISSING_LOAD_SET") != std::string::npos);
|
||||||
|
|
||||||
|
const auto* missing_node = findDiagnostic(diagnostics, "FESA-VALIDATION-NSET-MISSING-NODE");
|
||||||
|
FESA_CHECK(missing_node != nullptr);
|
||||||
|
FESA_CHECK(missing_node->message.find("BAD_NODES") != std::string::npos);
|
||||||
|
FESA_CHECK(missing_node->message.find("99") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
FESA_TEST(domain_validation_reports_nonpositive_thickness_and_invalid_dofs) {
|
||||||
|
auto domain = singleElementValidationDomain();
|
||||||
|
domain.shell_sections.front().thickness = 0.0;
|
||||||
|
domain.boundary_conditions.push_back({"ALL_NODES", 0, 1, 0.0});
|
||||||
|
domain.loads.push_back({"2", 7, 1.0});
|
||||||
|
|
||||||
|
auto diagnostics = fesa::validateDomain(domain);
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-NONPOSITIVE-THICKNESS"));
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-BOUNDARY-DOF"));
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-CLOAD-DOF"));
|
||||||
|
|
||||||
|
const auto* thickness = findDiagnostic(diagnostics, "FESA-VALIDATION-NONPOSITIVE-THICKNESS");
|
||||||
|
FESA_CHECK(thickness != nullptr);
|
||||||
|
FESA_CHECK(thickness->message.find("EALL") != std::string::npos);
|
||||||
|
|
||||||
|
const auto* load_dof = findDiagnostic(diagnostics, "FESA-VALIDATION-CLOAD-DOF");
|
||||||
|
FESA_CHECK(load_dof != nullptr);
|
||||||
|
FESA_CHECK(load_dof->message.find("DOF 7") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
FESA_TEST(domain_validation_reports_no_active_elements_and_missing_load) {
|
||||||
|
fesa::Domain domain;
|
||||||
|
domain.nodes[1] = {1, {0, 0, 0}};
|
||||||
|
auto diagnostics = fesa::validateDomain(domain);
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-SINGULAR-NO-ACTIVE-ELEMENTS"));
|
||||||
|
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-SINGULAR-NO-NONZERO-LOAD"));
|
||||||
|
for (const auto& diagnostic : diagnostics) {
|
||||||
|
FESA_CHECK(diagnostic.code.find("MESH") == std::string::npos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FESA_TEST(domain_validation_reports_no_free_dofs) {
|
||||||
|
auto domain = singleElementValidationDomain();
|
||||||
|
domain.boundary_conditions.push_back({"ALL_NODES", 1, 6, 0.0});
|
||||||
|
|
||||||
|
auto diagnostics = fesa::validateDomain(domain);
|
||||||
|
const auto* no_free = findDiagnostic(diagnostics, "FESA-SINGULAR-NO-FREE-DOFS");
|
||||||
|
FESA_CHECK(no_free != nullptr);
|
||||||
|
FESA_CHECK(no_free->source.keyword == "dof");
|
||||||
|
FESA_CHECK(no_free->message.find("No free DOFs") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
FESA_TEST(domain_validation_reports_free_untouched_dofs_for_isolated_nodes) {
|
||||||
|
auto domain = singleElementValidationDomain();
|
||||||
|
domain.nodes[99] = {99, {10, 0, 0}};
|
||||||
|
domain.boundary_conditions.push_back({"ALL_NODES", 1, 6, 0.0});
|
||||||
|
|
||||||
|
auto diagnostics = fesa::validateDomain(domain);
|
||||||
|
const auto* untouched = findDiagnostic(diagnostics, "FESA-SINGULAR-DOF-UNTOUCHED");
|
||||||
|
FESA_CHECK(untouched != nullptr);
|
||||||
|
FESA_CHECK(untouched->source.keyword == "dof");
|
||||||
|
FESA_CHECK(untouched->message.find("Node 99") != std::string::npos);
|
||||||
|
FESA_CHECK(untouched->message.find("UX") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
FESA_TEST(domain_validation_reports_weak_drilling_dof_smoke) {
|
||||||
|
auto domain = singleElementValidationDomain();
|
||||||
|
domain.boundary_conditions.push_back({"ALL_NODES", 1, 5, 0.0});
|
||||||
|
|
||||||
|
auto diagnostics = fesa::validateDomain(domain);
|
||||||
|
const auto* weak_drilling = findDiagnostic(diagnostics, "FESA-SINGULAR-WEAK-DRILLING-DOF");
|
||||||
|
FESA_CHECK(weak_drilling != nullptr);
|
||||||
|
FESA_CHECK(weak_drilling->severity == fesa::Severity::Warning);
|
||||||
|
FESA_CHECK(weak_drilling->source.keyword == "dof");
|
||||||
|
FESA_CHECK(weak_drilling->message.find("RZ") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
FESA_TEST(dof_manager_owns_equation_numbering_and_reconstruction) {
|
FESA_TEST(dof_manager_owns_equation_numbering_and_reconstruction) {
|
||||||
auto domain = parsedPhase1Domain();
|
auto domain = parsedPhase1Domain();
|
||||||
fesa::DofManager dofs(domain);
|
fesa::DofManager dofs(domain);
|
||||||
|
|||||||
Reference in New Issue
Block a user