feat(hex8): integrate paired HHT dynamic state and bounded verification
This commit is contained in:
@@ -434,7 +434,7 @@ HHT 식·기준 consistent M·입력·고정 schedule·100회·Si/exact-zero·
|
||||
- [x] Solid Domain/parser/3N DOF/ordered assembly/amplitude 검증
|
||||
- [x] 선형·비선형 정적 accepted state/반력/잔차 검증
|
||||
- [x] Generalized consistent-mass frequency·최저 N·original-pencil 검증
|
||||
- [ ] 선형·비선형 HHT u/v/physical a·시간수렴·수지 검증
|
||||
- [x] 선형·비선형 HHT u/v/physical a·시간수렴·수지 내부 검증
|
||||
- [ ] 공식 HDF5/CLI/self-check/실패 final 보존 검증
|
||||
- [ ] 정적2·동적2 원본 필수 family 비교 PASS
|
||||
- [ ] 사용자 원본 최저10 modal Value 제공 및 필수 비교 PASS
|
||||
@@ -446,9 +446,14 @@ Local kernel과 입력·3N mapping은 독립 검토 및 연결 후 관련180개
|
||||
native CTest exit0이며 독립 수치 검토를 통과했다. 두 정적 원본과442절점 고유치 원본은
|
||||
직접 API로 실행했지만 원본 결과값 비교를 수행한 것은 아니다. 연결 source `28be39f`의
|
||||
전체 MSVC Debug build는 exit0이며 이 build 자체를 전체 CTest 통과로 해석하지 않는다.
|
||||
HHT 내부 사례는 진행 중이다. 별도 구현 `f661864`의 관련76개 검사와 독립 수치 검토는
|
||||
통과했지만, 두 동적 원본은 세 번째 증분100회 제한에서 엄격 잔차 기준을 만족하지 못했다.
|
||||
최신 잔차 비율은 선형 `1.77739e-8`, 비선형 `8.73572e-9`이며 기준은 `1e-10`이다.
|
||||
동적 검증은 미완료이며 원인과 추가 검증은 [lessons](lessons.md)에 기록한다.
|
||||
과거 단일 Float64 구현 `f661864`는 관련76개 검사와 독립 수치 검토를 통과했지만,
|
||||
두 동적 원본의 세 번째 증분에서 strict gate를 만족하지 못했다. 이 실패는
|
||||
[lessons](lessons.md)에 보존하며 현재 방식의 결과와 구분한다.
|
||||
승인된 전체 high+low 구현 `c49cae4`는 소규모32/32 및 별도 성공한 LIN/TL·SDOF/MDOF
|
||||
시간수렴 사례와 독립 numerical/physics 검토를 확보했다. Root 연결 후 관련49/49와
|
||||
추가 직렬/TBB low 일치1/1 검사도 native build/CTest0/0이다. 두 원본의 초기 상태와
|
||||
증분1–3은 unchanged schedule로 관측했고, 원래 모든 free 성분의 전체 pair 물리 gate를
|
||||
독립 재계산했다. 이 관측은 네 번째 증분 전에 종료했으며 전체101 endpoint 완료가 아니다.
|
||||
상세 범위와 한계는 [수치 검토](numerical-review.md), [물리 검토](physics-review.md)에 있다.
|
||||
공식 결과·외부 비교·최신 전체 회귀도 남아 있다.
|
||||
원본 frequency 결과 부재는 구현 진행을 막지 않지만 기능 검증 완료를 막는다.
|
||||
|
||||
@@ -259,3 +259,16 @@ trial에서 계산한다. 독립 reviewer가 이 캐시와 native44/44의 원시
|
||||
독립 numerical reviewer는 실제 shared arithmetic·kernel·geometry의 한정된 범위를 PASS로
|
||||
판정했다. 재현 로그는 `.harness/hex8-evidence/precision/`에 보존한다. 이 결과는 동적 전체
|
||||
상태·시간수렴·원본 endpoint·공식 출력의 완료를 뜻하지 않는다. 현재 checklist는 미완료다.
|
||||
|
||||
## 전체 상태의 제한적 원본 관측과 연결
|
||||
|
||||
전체 high+low T6 `c49cae4`는 원래 시간 간격·INC를 바꾸지 않고 두 원본의 초기 상태와
|
||||
증분1–3을 accept했다. Test-only typed stop으로 다음 증분 전에 종료해 prefix 상태를
|
||||
관측했으며 native0·2/2를 전체 원본 완료로 보고하지 않는다. 메인이 전체 pair·full M/Mff,
|
||||
모든1275 free r/Si·zero branch·HHT update·weighted old-r·실제 solve 보정을 exact Fraction으로
|
||||
검산했다. 물리 비율 최대값은 선형3.12680e-20, 비선형1.3043731e-16이다.
|
||||
|
||||
Root 연결의 related49/49와 두 brick의 LIN/TL 전체 low 직렬/TBB 일치1/1은 native0이다.
|
||||
최초 broad filter가 긴 전체 원본 smoke를 함께 선택한 실행은 의도적으로 중단했고
|
||||
CTest exit−1로 보존했다. 완료하지 않은 실행을 수렴 실패로 해석하거나 현재 PASS에 합산하지
|
||||
않는다. 공식 출력·101 endpoint·원본 비교·전체 회귀는 후속 검증이다.
|
||||
|
||||
@@ -20,6 +20,37 @@ T6 consumer·backend·worker 경계와 rollback은 해당 구현이 고정된
|
||||
동일 graph·선형 force/energy/recovery·비선형 trial Hessian·기존 F64 Create 지원을 유지하며
|
||||
native44/44 원시 로그를 확인했다. 이 캐시 변경도 bounded PASS다.
|
||||
|
||||
## 2026-10-10 — 전체 동적 상태와 원본 세 증분의 제한적 검토
|
||||
|
||||
동일 독립 reviewer가 고정된 T6 `c49cae4`의 predictor/rates, 기준 M의 전체 pair 작용,
|
||||
ordered force/tangent 누적, full residual·scale, 실제 old residual을 포함한 weighted 식,
|
||||
fresh solve와 실제 상태 변경의 네 보정 채널, backend/worker 이후 실행 전제 검사,
|
||||
accepted low의 복사·검증·rollback을 검토했다. 원시73개 inventory와 owned/shared source,
|
||||
binary·patch의 hash를 확인했고 해당 consumer 범위 bounded PASS, actionable finding0이다.
|
||||
Current32/32 검사와 과거 temporal4/5 실행의 성공 사례를 구분한다.
|
||||
|
||||
이후 변경하지 않은 두 원본의 초기 상태와 증분1–3을 관측했다. 원래 h=0.1,T=10,INC=100,
|
||||
1326 full/1275 free/51 constrained를 유지하고, 네 번째 증분 local 계산 전 test-only 예외로
|
||||
종료했다. 관측 native0·2/2는 이 종료 조건의 성공이며 전체 해석 완료가 아니다.
|
||||
메인은51개 raw artifact의 hash/크기를 확인한 뒤 두 binary64 성분을 exact dyadic Fraction으로
|
||||
읽어 모든 상태의 normalization, 실제 Mff의 full M restriction, 원래 모든 free 성분의
|
||||
물리 r/Si와 exact-zero, HHT 세 update 식과 weighted old-r 항등식을 독립 재계산했다.
|
||||
|
||||
Exact CSR M 작용으로 재계산한 물리 비율의 최댓값은 선형3.12680e-20,
|
||||
비선형1.3043731e-16으로 승인1e-10 이내다. Update·weighted identity는1e-12 이내이며
|
||||
초기1275개 free row는 모두 exact0이다. 실제 PARDISO 보정의 선형6회·비선형9회 기록으로
|
||||
Newton 시작값인 이전 accepted u부터 최종 u를 재구성하고, fresh/observed 네 보정 채널도
|
||||
각각 확인했다. 비선형 최대 보정 비율1.01046e-13은 승인1e-10 이내다.
|
||||
|
||||
Storage graph 대조의 별도 절대 오차 budget과 물리 gate를 혼동하지 않는다. Exact M 작용과
|
||||
저장된 pair Ma의 상대 차이는 최대2.88e-32였다. Strict physical gate에 floor를 추가하거나
|
||||
high만으로 full 상태를 대신하지 않았다. 재현 자료는 로컬
|
||||
`.harness/hex8-evidence/precision/{prefix3-frozen,prefix-independent.json,check_prefix.py}`다.
|
||||
Root 연결 후 관련49/49 검사의 native build/CTest는0/0이다. 이어 두 brick의 LIN/TL 직렬·TBB
|
||||
전체 low 필드 일치와 실제 nonzero low를 확인한 focused1/1 검사도0/0이다.
|
||||
최초 넓은 filter가 전체 원본 smoke를 선택해 중단한 실행은 CTest exit−1로 보존하며
|
||||
수렴 실패 또는 통과로 해석하지 않는다. 전체101 endpoint·공식 HDF5·원본 비교는 아직 없다.
|
||||
|
||||
## HEX8-NR-001 — 영 회전 경계와 필수 원본 충돌
|
||||
|
||||
검토 당시 PRD008/I/O는 BC/CLOAD DOF4–6을 값0이어도 거부했다. 그러나 모든 필수 INP에
|
||||
|
||||
@@ -35,7 +35,30 @@ temporal 실행은4/5 PASS·CTest exit8이며 실패는 이전 principal trace a
|
||||
owned/shared source·원본 두 INP·binary·patch hash를 대조했다. Reviewer는 관련22개 raw와
|
||||
7개 owned source·patch를 별도로 대조했다.
|
||||
|
||||
전체 high+low endpoint 배열의 독립 gate 재계산과 원본 prefix/101 endpoint·필수 reference
|
||||
비교는 아직 수행하지 않았다. Rounded observable 불변량 검사와 전체 pair의 production
|
||||
첫32개 검토 이후 원본 prefix의 전체 pair를 아래와 같이 별도 재계산했다. 전체101 endpoint·
|
||||
필수 reference 비교는 아직 없다. Rounded observable 불변량 검사와 전체 pair의 production
|
||||
gate 통과를 구분한다. Alpha−0.05의 장시간 에너지, warped dynamic mesh,
|
||||
near-incompressibility 및 큰 strain StVK 적용은 이번 evidence로 보증하지 않는다.
|
||||
|
||||
## 원본 초기 상태와 세 증분의 독립 물리 계산
|
||||
|
||||
변경하지 않은 LIN/TL 두 원본의 초기 상태와 증분1–3에 대해 reviewer가 모든 high+low를
|
||||
exact Fraction으로 읽었다. 기준 CSR M·reference X·실제 full force를 사용해 T/W/balance,
|
||||
p/pdot/L/Ldot, reference/current/free-residual moments와 global force/moment 수지를
|
||||
총240개 대조했다. 실제 pair Add의 discarded-tail bound를 전파한 별도 절대 graph budget
|
||||
내였으며 재생 graph의 high/low도240/240 일치했다. 구속 u/v/a/A와 support work는 exact0다.
|
||||
Source442절점·300연결·51구속·하중·원래 h/T/INC와 test-only 관측 종료 조건도 대조했다.
|
||||
선형 U와 독립0.5uᵀf의 최대 절대차는1.30e-29였다.
|
||||
|
||||
Captured F64 M의 ordered coefficient에는7752개의 비대칭 entry가 있고
|
||||
최대|Mij−Mji|=2.84217e-14다. 이에 따른 exact Σv×Mv의 작은 비영 값은512eps same-unit
|
||||
coefficient-roundoff budget 이내이며 이 비대칭으로 정확히 설명된다. Pair 산술 오류와
|
||||
계수 오차를 구분하고 M을 대칭화하거나 모멘트를0으로 보정하지 않는다. Ldot=x×Ma는
|
||||
같은 endpoint의 계약 observable로 확인했다. LIN의 current internal moment는 O(u²)로
|
||||
비영이며 TL의 reference moment도 별도0 기대량이 아니다.
|
||||
|
||||
Alpha−0.05의 frame별 energy balance는 관측량이지 exact conservation gate가 아니다.
|
||||
Prefix에서 나타난 balance와 진동에 따른 trapezoid work 감소를 그대로 보존한다.
|
||||
이 범위에서 물리 blocker는 없다. 재현 자료는 로컬
|
||||
`.harness/hex8-evidence/precision/physics-prefix-review/`와 `prefix3-frozen/`다.
|
||||
TL의 local polynomial U 검산은 별도 frozen reference cache에 대해 추가 확인한다.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Hex8 동해석의 엄격 잔차와 내부 상태 정밀도
|
||||
|
||||
- 조건·현상: Hex8 동적 구현 `f661864`에서 `reference/hexa/hexa_dynlinear.inp`와
|
||||
`hexa_dynnonlinear.inp`를 실행하면 세 번째 증분100회 제한에서 실패한다. 최신 원본
|
||||
`hexa_dynnonlinear.inp`를 실행하면 세 번째 증분100회 제한에서 실패했다. 이 과거 원본
|
||||
physical residual 비율은 `1.77739e-8`·`8.73572e-9`, 기준은 `1e-10`이다.
|
||||
- 영향: 원본101개 endpoint의 필수 검증과 동해석 완료 조건을 충족하지 못했다.
|
||||
현재 전체 요구와 checklist는 [기능 PRD](../hex8-structural-analysis/PRD.md)에 있다.
|
||||
@@ -12,9 +12,10 @@
|
||||
Float64 해의 부재를 증명하지 않으며, 수렴 기준·원본을 변경할 근거로 쓰지 않는다.
|
||||
- 후속 방향: [실패 근거](../hex8-structural-analysis/lessons.md)를 보존하고, 전체 물리
|
||||
상태의 high+low 계산과 Float64 출력의 반올림 의미를 정하는
|
||||
[승인된 변경안](../hex8-structural-analysis/precision-proposal.md)을 구현한다.
|
||||
사용자가 2026-10-09 선택 A로 승인했다. 산술·요소의 첫 native 검사는 통과했으나
|
||||
전체 동적 state·시간수렴·원본 endpoint 검증은 남아 있다. Acceptance 기준은 유지한다.
|
||||
[승인된 변경안](../hex8-structural-analysis/precision-proposal.md)을 구현했다.
|
||||
사용자가 2026-10-09 선택 A로 승인했고 소규모 상태·시간수렴·수지와 두 원본의 세 증분
|
||||
관측을 검증했다. 독립 full-pair 재계산에서 원래 모든 free의 strict gate를 통과했다.
|
||||
전체101 endpoint·공식 출력·필수 원본 비교는 남아 있으며 acceptance 기준은 유지한다.
|
||||
|
||||
## HHT shell의 시간수렴
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
#ifndef FESA_ANALYSIS_SOLID_DYNAMIC_ANALYSIS_STATE_H_
|
||||
#define FESA_ANALYSIS_SOLID_DYNAMIC_ANALYSIS_STATE_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/results/solid_result_records.h"
|
||||
|
||||
namespace fesa {
|
||||
|
||||
/// @brief Dimensionally explicit translation-only Newton evidence.
|
||||
struct SolidDynamicIterationEvidence {
|
||||
std::uint32_t iteration_index{0};
|
||||
double residual_numerator{0}; // Force infinity norm.
|
||||
double residual_scale{0}; // Uncancelled force infinity scale.
|
||||
double residual_metric{0}; // Maximum componentwise force ratio.
|
||||
std::size_t worst_full_dof{0};
|
||||
std::array<double, 5> worst_force_terms{}; // r,Ma,fint,F,uncancelled scale.
|
||||
double weighted_residual_numerator{0}; // Original HHT force infinity norm.
|
||||
double recombined_residual_numerator{
|
||||
0}; // Actual solved weighted force norm.
|
||||
double old_residual_numerator{0}; // Previous physical free force norm.
|
||||
double weighted_equivalence_metric{0}; // Uncancelled force identity ratio.
|
||||
std::array<double, 4>
|
||||
worst_weighted_terms{}; // Original,recombined,old r,scale.
|
||||
bool zero_scale_exact{true};
|
||||
std::array<double, 4> correction_metrics{}; // du/L,h*dv/L,h²*da/L,h²*dA/L.
|
||||
double update_metric{0}; // Maximum uncancelled update equality ratio.
|
||||
bool residual_pass{false};
|
||||
bool correction_pass{false};
|
||||
bool update_pass{false};
|
||||
};
|
||||
|
||||
/// @brief Failure-only shadow dot products; these never change force or
|
||||
/// acceptance.
|
||||
struct SolidDynamicLinearDotEvidence {
|
||||
EntityIndex element_index{0};
|
||||
std::size_t local_row{0};
|
||||
double absolute_product_sum{0};
|
||||
double kernel_force{0};
|
||||
double compensated_force{
|
||||
0}; // Shadow using rounded relative differences only.
|
||||
};
|
||||
|
||||
/// @brief Failure-only coordinate arithmetic evidence, with length units.
|
||||
struct SolidDynamicCoordinateEvidence {
|
||||
EntityIndex element_index{0};
|
||||
std::size_t local_row{0};
|
||||
double displacement{0};
|
||||
double previous_displacement{0};
|
||||
double fresh_correction{0};
|
||||
double reference_displacement{0};
|
||||
double previous_reference_displacement{0};
|
||||
double relative_high{0};
|
||||
double previous_relative_high{0};
|
||||
};
|
||||
|
||||
/// @brief Failure-only arithmetic shadow; never substitutes physical forces.
|
||||
struct SolidDynamicForceSumEvidence {
|
||||
double assembled_internal{0};
|
||||
double compensated_internal{0};
|
||||
double absolute_local_internal_sum{0};
|
||||
double assembled_inertia{0};
|
||||
double compensated_inertia{0};
|
||||
double absolute_inertia_product_sum{0};
|
||||
};
|
||||
|
||||
/// @brief Internal exact-candidate recurrence evidence, separate from gates.
|
||||
struct SolidDynamicNewtonRepeatEvidence {
|
||||
std::size_t history_entry_count{0};
|
||||
std::uint32_t matched_entry_iteration{0};
|
||||
std::size_t changed_translations{0};
|
||||
std::size_t changed_holding_mask{0};
|
||||
std::size_t changed_correction_metrics{0};
|
||||
bool repeat{false};
|
||||
};
|
||||
|
||||
/// @brief Keeps failed attempt evidence outside the accepted physical history.
|
||||
struct SolidDynamicIterationRecord {
|
||||
std::uint32_t increment_index{0};
|
||||
double time{0};
|
||||
std::string stage;
|
||||
std::string outcome;
|
||||
std::string failure_code;
|
||||
SolidDynamicIterationEvidence evidence;
|
||||
std::vector<SolidDynamicLinearDotEvidence> failure_linear_dot;
|
||||
std::vector<SolidDynamicCoordinateEvidence> failure_coordinates;
|
||||
std::optional<SolidDynamicForceSumEvidence> failure_force_sum;
|
||||
std::optional<SolidDynamicNewtonRepeatEvidence> newton_repeat;
|
||||
};
|
||||
|
||||
/// @brief Retains normalized physical low parts; never serialized as extra
|
||||
/// result fields. Corresponding endpoint fields contain the nearest Float64
|
||||
/// high parts. Both parts are copied and validated together at commit.
|
||||
struct SolidDynamicFrameLowParts {
|
||||
Vector displacement{0};
|
||||
Vector velocity{0};
|
||||
Vector acceleration{0};
|
||||
Vector algorithmic_acceleration{0};
|
||||
Vector inertia_force{0};
|
||||
Vector internal_force{0};
|
||||
Vector external_force{0};
|
||||
Vector residual{0};
|
||||
Vector reaction{0};
|
||||
Vector residual_scale{0};
|
||||
double strain_energy{0};
|
||||
double kinetic_energy{0};
|
||||
double external_work{0};
|
||||
double energy_balance_error{0};
|
||||
std::array<double, 3> linear_momentum{};
|
||||
std::array<double, 3> angular_momentum{};
|
||||
std::array<double, 3> linear_momentum_rate{};
|
||||
std::array<double, 3> angular_momentum_rate{};
|
||||
std::array<double, 6> global_equilibrium{};
|
||||
std::array<double, 3> linearization_current_internal_moment{};
|
||||
std::array<double, 3> reference_internal_moment{};
|
||||
std::array<double, 3> current_free_residual_moment{};
|
||||
};
|
||||
|
||||
/// @brief Owns one complete Solid endpoint; physical a differs from auxiliary
|
||||
/// A.
|
||||
struct SolidDynamicAcceptedFrameState {
|
||||
std::uint32_t frame_index{0};
|
||||
std::uint32_t increment_index{0};
|
||||
std::uint32_t iteration_count{0};
|
||||
double time{0};
|
||||
bool is_initial{false};
|
||||
std::vector<EntityIndex> nodal_internal_order;
|
||||
std::vector<SourceEntityId> nodal_source_ids;
|
||||
Vector displacement{0};
|
||||
Vector velocity{0};
|
||||
Vector acceleration{0};
|
||||
Vector algorithmic_acceleration{0};
|
||||
Vector inertia_force{0};
|
||||
Vector internal_force{0};
|
||||
Vector external_force{0};
|
||||
Vector residual{0};
|
||||
Vector reaction{0};
|
||||
Vector residual_scale{0};
|
||||
std::vector<SolidIntegrationPointRow> solid_results;
|
||||
double strain_energy{0};
|
||||
double kinetic_energy{0};
|
||||
double external_work{0};
|
||||
double energy_balance_error{0};
|
||||
std::array<double, 3> linear_momentum{};
|
||||
std::array<double, 3> angular_momentum{};
|
||||
std::array<double, 3> linear_momentum_rate{};
|
||||
std::array<double, 3> angular_momentum_rate{};
|
||||
std::array<double, 6> global_equilibrium{};
|
||||
// Internal evidence, not additional official output fields.
|
||||
std::array<double, 3> linearization_current_internal_moment{};
|
||||
std::array<double, 3> reference_internal_moment{};
|
||||
std::array<double, 3> current_free_residual_moment{};
|
||||
std::vector<SolidDynamicIterationEvidence> iterations;
|
||||
SolidDynamicFrameLowParts low;
|
||||
};
|
||||
|
||||
/// @brief Owns only validated accepted Solid history; rejects partial
|
||||
/// candidates.
|
||||
class SolidDynamicAnalysisState {
|
||||
public:
|
||||
/// @brief Creates an empty state for exactly one zero-BC fixed dynamic
|
||||
/// schedule.
|
||||
static Result<SolidDynamicAnalysisState> Create(const Domain& domain,
|
||||
const DofManager& dofs);
|
||||
/// @brief Validates shape, identity, schedule, finite values and Newton
|
||||
/// evidence.
|
||||
/// @note A failed candidate never changes accepted frames or current state.
|
||||
Status CommitAcceptedFrame(const SolidDynamicAcceptedFrameState& frame);
|
||||
/// @brief Returns an independent accepted endpoint copy for trial
|
||||
/// preparation.
|
||||
Result<SolidDynamicAcceptedFrameState> BeginTrialFromCommitted() const;
|
||||
/// @brief Returns accepted endpoints in source increment order.
|
||||
const std::vector<SolidDynamicAcceptedFrameState>& AcceptedFrames()
|
||||
const noexcept;
|
||||
/// @brief Returns the last endpoint; throws when no frame is accepted.
|
||||
const SolidDynamicAcceptedFrameState& CommittedState() const;
|
||||
/// @brief Reports whether schedule and inventory have been initialized.
|
||||
bool IsInitialized() const noexcept;
|
||||
/// @brief Appends attempt evidence without committing physical state.
|
||||
void RecordIteration(SolidDynamicIterationRecord record);
|
||||
/// @brief Returns attempt evidence, including a terminal failure if present.
|
||||
const std::vector<SolidDynamicIterationRecord>& IterationRecords()
|
||||
const noexcept;
|
||||
|
||||
private:
|
||||
std::size_t full_dof_count_{0};
|
||||
std::uint32_t increment_count_{0};
|
||||
double time_increment_{0};
|
||||
std::vector<std::size_t> constrained_dofs_;
|
||||
std::vector<SourceEntityId> source_ids_;
|
||||
std::vector<SolidIntegrationPointRow> point_inventory_;
|
||||
std::vector<SolidDynamicAcceptedFrameState> accepted_frames_;
|
||||
std::vector<SolidDynamicIterationRecord> iteration_records_;
|
||||
bool initialized_{false};
|
||||
};
|
||||
} // namespace fesa
|
||||
#endif // FESA_ANALYSIS_SOLID_DYNAMIC_ANALYSIS_STATE_H_
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef FESA_ANALYSIS_SOLID_HHT_DYNAMIC_PROCEDURE_H_
|
||||
#define FESA_ANALYSIS_SOLID_HHT_DYNAMIC_PROCEDURE_H_
|
||||
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
|
||||
namespace fesa {
|
||||
class LinearSolver;
|
||||
class ParallelFor;
|
||||
class TangentSolver;
|
||||
|
||||
/// @brief Owns the validated 3N dynamic result independently of serialization.
|
||||
struct SolidDynamicExecutionResult {
|
||||
DofManager dofs;
|
||||
SolidDynamicAnalysisState state;
|
||||
};
|
||||
|
||||
/// @brief Integrates linear or TL Solid mechanics with constant consistent
|
||||
/// mass.
|
||||
class SolidHhtDynamicProcedureExecutor {
|
||||
public:
|
||||
/// @brief Retains caller-owned adapters which must outlive execution.
|
||||
SolidHhtDynamicProcedureExecutor(const ParallelFor& parallel_for,
|
||||
LinearSolver& mass_solver,
|
||||
TangentSolver& tangent_solver);
|
||||
/// @brief Returns complete accepted history without opening output files.
|
||||
Result<SolidDynamicExecutionResult> Execute(const Domain& domain) const;
|
||||
/// @brief Executes into an empty initialized state, preserving accepted
|
||||
/// history on every failure. Domain must match the state inventory and
|
||||
/// schedule.
|
||||
Status ExecuteInto(const Domain& domain,
|
||||
SolidDynamicAnalysisState& state) const;
|
||||
/// @brief Internal verification only; alpha0 verifies conservative dynamics.
|
||||
/// @note This does not extend the approved INP parameter subset.
|
||||
Status ExecuteIntoForVerification(const Domain& domain,
|
||||
SolidDynamicAnalysisState& state,
|
||||
double alpha) const;
|
||||
|
||||
private:
|
||||
const ParallelFor& parallel_for_;
|
||||
LinearSolver& mass_solver_;
|
||||
TangentSolver& tangent_solver_;
|
||||
};
|
||||
} // namespace fesa
|
||||
#endif // FESA_ANALYSIS_SOLID_HHT_DYNAMIC_PROCEDURE_H_
|
||||
@@ -17,7 +17,9 @@ add_library(
|
||||
analysis/nonlinear_static_control.cpp
|
||||
analysis/nonlinear_static_procedure.cpp
|
||||
analysis/prepared_analysis.cpp
|
||||
analysis/solid_dynamic_analysis_state.cpp
|
||||
analysis/solid_frequency_procedure.cpp
|
||||
analysis/solid_hht_dynamic_procedure.cpp
|
||||
analysis/solid_static_procedure.cpp
|
||||
analysis/solid_static_state.cpp
|
||||
analysis/thermal_analysis_state.cpp
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/hex8_element.h"
|
||||
#include "math/high_low_arithmetic.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
/// @brief Rejects a candidate without changing observable history.
|
||||
Status Failure(const std::string& message) {
|
||||
return Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError,
|
||||
"solid-dynamic-invalid-state",
|
||||
{},
|
||||
"DYNAMIC",
|
||||
{},
|
||||
message}});
|
||||
}
|
||||
/// @brief Checks a physical record without replacing nonfinite quantities.
|
||||
template <std::size_t N>
|
||||
bool Finite(const std::array<double, N>& values) {
|
||||
return std::all_of(values.begin(), values.end(),
|
||||
[](double x) { return std::isfinite(x); });
|
||||
}
|
||||
bool ValidPair(double high, double low) {
|
||||
const math_internal::HighLow value(high, low);
|
||||
return math_internal::IsFinite(value) && math_internal::IsNormalized(value);
|
||||
}
|
||||
bool ValidPair(const Vector& high, const Vector& low, std::size_t size) {
|
||||
if (high.Size() != size || low.Size() != size) return false;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
if (!ValidPair(high[i], low[i])) return false;
|
||||
return true;
|
||||
}
|
||||
template <std::size_t N>
|
||||
bool ValidPair(const std::array<double, N>& high,
|
||||
const std::array<double, N>& low) {
|
||||
for (std::size_t i = 0; i < N; ++i)
|
||||
if (!ValidPair(high[i], low[i])) return false;
|
||||
return true;
|
||||
}
|
||||
/// @brief Preserves canonical and raw source identity independently of indices.
|
||||
bool SameId(const SourceEntityId& a, const SourceEntityId& b) {
|
||||
return a.instance_name == b.instance_name &&
|
||||
a.source_label == b.source_label &&
|
||||
a.source_label_text == b.source_label_text;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Result<SolidDynamicAnalysisState> SolidDynamicAnalysisState::Create(
|
||||
const Domain& domain, const DofManager& dofs) {
|
||||
const auto* step = domain.Dynamic();
|
||||
if (!domain.IsSolid() || step == nullptr ||
|
||||
dofs.Profile() != DofProfile::kSolidStructural ||
|
||||
dofs.FullDofCount() != 3 * domain.Nodes().size() ||
|
||||
!dofs.ValidateInvariants().IsOk() || step->increment_count == 0 ||
|
||||
!std::isfinite(step->initial_increment) || step->initial_increment <= 0 ||
|
||||
!std::isfinite(step->time_period) ||
|
||||
static_cast<double>(step->increment_count) * step->initial_increment !=
|
||||
step->time_period ||
|
||||
step->increment_count > step->maximum_increments) {
|
||||
return Result<SolidDynamicAnalysisState>::Failure(
|
||||
Failure("Solid HHT requires a valid 3N exact fixed schedule."));
|
||||
}
|
||||
for (std::size_t i = 0; i < dofs.PrescribedValues().Size(); ++i)
|
||||
if (dofs.PrescribedValues()[i] != 0)
|
||||
return Result<SolidDynamicAnalysisState>::Failure(
|
||||
Failure("Dynamic prescribed translations must be exactly zero."));
|
||||
SolidDynamicAnalysisState state;
|
||||
state.full_dof_count_ = dofs.FullDofCount();
|
||||
state.increment_count_ = step->increment_count;
|
||||
state.time_increment_ = step->initial_increment;
|
||||
state.constrained_dofs_ = dofs.ConstrainedDofs();
|
||||
for (const auto& node : domain.Nodes())
|
||||
state.source_ids_.push_back(node.source_id);
|
||||
ElementFactory factory;
|
||||
for (std::size_t e = 0; e < domain.Elements().Size(); ++e) {
|
||||
auto runtime = factory.Create(domain.Elements()[e], domain);
|
||||
if (!runtime.HasValue())
|
||||
return Result<SolidDynamicAnalysisState>::Failure(runtime.GetStatus());
|
||||
const auto* solid = dynamic_cast<const Hex8Element*>(runtime.Value().get());
|
||||
if (solid == nullptr)
|
||||
return Result<SolidDynamicAnalysisState>::Failure(Failure(
|
||||
"Dynamic Solid inventory contains a different element family."));
|
||||
auto zero = solid->EvaluateLinear(Vector(24));
|
||||
if (!zero.HasValue())
|
||||
return Result<SolidDynamicAnalysisState>::Failure(zero.GetStatus());
|
||||
state.point_inventory_.insert(state.point_inventory_.end(),
|
||||
zero.Value().points.begin(),
|
||||
zero.Value().points.end());
|
||||
}
|
||||
state.initialized_ = true;
|
||||
return Result<SolidDynamicAnalysisState>::Success(std::move(state));
|
||||
}
|
||||
|
||||
Status SolidDynamicAnalysisState::CommitAcceptedFrame(
|
||||
const SolidDynamicAcceptedFrameState& f) {
|
||||
if (!math_internal::ArithmeticEnvironmentSupported())
|
||||
return Failure(
|
||||
"Paired state commit requires nearest rounding and gradual "
|
||||
"underflow.");
|
||||
const std::size_t index = accepted_frames_.size();
|
||||
if (!initialized_ || index > increment_count_ || f.frame_index != index ||
|
||||
f.increment_index != index || f.is_initial != (index == 0) ||
|
||||
f.time != static_cast<double>(index) * time_increment_ ||
|
||||
f.iteration_count > 100 || (index == 0 && f.iteration_count != 0) ||
|
||||
f.nodal_internal_order.size() != source_ids_.size() ||
|
||||
f.nodal_source_ids.size() != source_ids_.size() ||
|
||||
f.solid_results.size() != point_inventory_.size()) {
|
||||
return Failure(
|
||||
"Incomplete or out-of-order Solid endpoint inventory/schedule.");
|
||||
}
|
||||
for (std::size_t n = 0; n < source_ids_.size(); ++n)
|
||||
if (f.nodal_internal_order[n] != n ||
|
||||
!SameId(f.nodal_source_ids[n], source_ids_[n]))
|
||||
return Failure("Solid nodal source identity differs from its inventory.");
|
||||
const std::array<const Vector*, 10> high{
|
||||
&f.displacement, &f.velocity,
|
||||
&f.acceleration, &f.algorithmic_acceleration,
|
||||
&f.inertia_force, &f.internal_force,
|
||||
&f.external_force, &f.residual,
|
||||
&f.reaction, &f.residual_scale};
|
||||
const std::array<const Vector*, 10> low{
|
||||
&f.low.displacement, &f.low.velocity,
|
||||
&f.low.acceleration, &f.low.algorithmic_acceleration,
|
||||
&f.low.inertia_force, &f.low.internal_force,
|
||||
&f.low.external_force, &f.low.residual,
|
||||
&f.low.reaction, &f.low.residual_scale};
|
||||
for (std::size_t i = 0; i < high.size(); ++i)
|
||||
if (!ValidPair(*high[i], *low[i], full_dof_count_))
|
||||
return Failure(
|
||||
"Solid endpoint pairs are incomplete, unnormalized or "
|
||||
"nonfinite.");
|
||||
using namespace math_internal;
|
||||
for (std::size_t i = 0; i < full_dof_count_; ++i) {
|
||||
const bool constrained = std::binary_search(constrained_dofs_.begin(),
|
||||
constrained_dofs_.end(), i);
|
||||
const HighLow inertia(f.inertia_force[i], f.low.inertia_force[i]);
|
||||
const HighLow internal(f.internal_force[i], f.low.internal_force[i]);
|
||||
const HighLow external(f.external_force[i], f.low.external_force[i]);
|
||||
const HighLow residual(f.residual[i], f.low.residual[i]);
|
||||
const HighLow scale(f.residual_scale[i], f.low.residual_scale[i]);
|
||||
const HighLow reaction(f.reaction[i], f.low.reaction[i]);
|
||||
const auto expected_residual = Subtract(Add(inertia, internal), external);
|
||||
const auto expected_scale =
|
||||
Add(Add(Abs(inertia), Abs(internal)), Abs(external));
|
||||
if (!IsFinite(expected_residual) || !IsFinite(expected_scale) ||
|
||||
Compare(residual, expected_residual) != 0 ||
|
||||
Compare(scale, expected_scale) != 0 ||
|
||||
Compare(reaction, constrained ? residual : HighLow(0)) != 0)
|
||||
return Failure(
|
||||
"Physical reaction/residual/uncancelled force scale disagree.");
|
||||
if (constrained && (f.displacement[i] != 0 || f.low.displacement[i] != 0 ||
|
||||
f.velocity[i] != 0 || f.low.velocity[i] != 0 ||
|
||||
f.acceleration[i] != 0 || f.low.acceleration[i] != 0 ||
|
||||
f.algorithmic_acceleration[i] != 0 ||
|
||||
f.low.algorithmic_acceleration[i] != 0))
|
||||
return Failure(
|
||||
"Prescribed dynamic translations and rates must be exact zero.");
|
||||
if (!constrained) {
|
||||
if (Compare(scale, HighLow(0)) < 0)
|
||||
return Failure("Negative physical force scale.");
|
||||
if (Compare(scale, HighLow(0)) == 0) {
|
||||
if (Compare(residual, HighLow(0)) != 0)
|
||||
return Failure("Physical zero-scale residual must be exactly zero.");
|
||||
} else {
|
||||
const auto ratio = Divide(Abs(residual), scale);
|
||||
if (!IsFinite(ratio) || !IsNormalized(ratio) ||
|
||||
Compare(ratio, HighLow(1e-10)) > 0)
|
||||
return Failure("Physical free endpoint equilibrium is incomplete.");
|
||||
}
|
||||
}
|
||||
if (index == 0 &&
|
||||
(f.displacement[i] != 0 || f.velocity[i] != 0 ||
|
||||
f.low.displacement[i] != 0 || f.low.velocity[i] != 0 ||
|
||||
Compare(HighLow(f.acceleration[i], f.low.acceleration[i]),
|
||||
HighLow(f.algorithmic_acceleration[i],
|
||||
f.low.algorithmic_acceleration[i])) != 0))
|
||||
return Failure(
|
||||
"Initial Solid displacement/velocity must be zero and A=a.");
|
||||
}
|
||||
for (std::size_t i = 0; i < point_inventory_.size(); ++i) {
|
||||
const auto& row = f.solid_results[i];
|
||||
const auto& expected = point_inventory_[i];
|
||||
if (row.element != expected.element ||
|
||||
row.integration_point != expected.integration_point ||
|
||||
row.natural_coordinates != expected.natural_coordinates ||
|
||||
row.reference_coordinates != expected.reference_coordinates ||
|
||||
!Finite(row.cauchy_stress) || !Finite(row.strain))
|
||||
return Failure(
|
||||
"Solid point recovery has invalid identity or finite values.");
|
||||
}
|
||||
if (!ValidPair(f.strain_energy, f.low.strain_energy) ||
|
||||
!ValidPair(f.kinetic_energy, f.low.kinetic_energy) ||
|
||||
!ValidPair(f.external_work, f.low.external_work) ||
|
||||
!ValidPair(f.energy_balance_error, f.low.energy_balance_error))
|
||||
return Failure("Nonfinite or unnormalized Solid energy/work pair.");
|
||||
if ((index == 0 &&
|
||||
(f.external_work != 0 || f.low.external_work != 0 ||
|
||||
f.energy_balance_error != 0 || f.low.energy_balance_error != 0)) ||
|
||||
Compare(HighLow(f.kinetic_energy, f.low.kinetic_energy), HighLow(0)) < 0)
|
||||
return Failure(
|
||||
"Initial work/error or kinetic energy violates its definition.");
|
||||
if (!ValidPair(f.linear_momentum, f.low.linear_momentum) ||
|
||||
!ValidPair(f.angular_momentum, f.low.angular_momentum) ||
|
||||
!ValidPair(f.linear_momentum_rate, f.low.linear_momentum_rate) ||
|
||||
!ValidPair(f.angular_momentum_rate, f.low.angular_momentum_rate) ||
|
||||
!ValidPair(f.linearization_current_internal_moment,
|
||||
f.low.linearization_current_internal_moment) ||
|
||||
!ValidPair(f.reference_internal_moment,
|
||||
f.low.reference_internal_moment) ||
|
||||
!ValidPair(f.current_free_residual_moment,
|
||||
f.low.current_free_residual_moment))
|
||||
return Failure("Nonfinite or unnormalized physical momentum/moment pair.");
|
||||
if (!ValidPair(f.global_equilibrium, f.low.global_equilibrium) ||
|
||||
f.iterations.empty())
|
||||
return Failure("Missing finite equilibrium or iteration evidence.");
|
||||
for (std::size_t i = 0; i < f.iterations.size(); ++i) {
|
||||
const auto& e = f.iterations[i];
|
||||
if (e.iteration_index != i || !Finite(e.correction_metrics) ||
|
||||
!Finite(e.worst_force_terms) || !Finite(e.worst_weighted_terms))
|
||||
return Failure("Incomplete or nonfinite Newton correction evidence.");
|
||||
for (double x : {e.residual_numerator, e.residual_scale, e.residual_metric,
|
||||
e.weighted_residual_numerator, e.update_metric,
|
||||
e.old_residual_numerator, e.recombined_residual_numerator,
|
||||
e.weighted_equivalence_metric})
|
||||
if (!std::isfinite(x) || x < 0)
|
||||
return Failure("Invalid raw Newton residual/update evidence.");
|
||||
for (double x : e.correction_metrics)
|
||||
if (x < 0) return Failure("Invalid correction scale.");
|
||||
}
|
||||
const auto& last = f.iterations.back();
|
||||
if (f.iterations.size() != static_cast<std::size_t>(f.iteration_count) + 1 ||
|
||||
!last.residual_pass || !last.correction_pass || !last.update_pass ||
|
||||
!last.zero_scale_exact || last.residual_metric > 1e-10 ||
|
||||
last.update_metric > 1e-12 || last.weighted_equivalence_metric > 1e-12 ||
|
||||
std::any_of(last.correction_metrics.begin(),
|
||||
last.correction_metrics.end(),
|
||||
[](double x) { return x > 1e-10; }))
|
||||
return Failure(
|
||||
"Candidate does not carry complete accepted convergence evidence.");
|
||||
accepted_frames_.push_back(f);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Result<SolidDynamicAcceptedFrameState>
|
||||
SolidDynamicAnalysisState::BeginTrialFromCommitted() const {
|
||||
if (accepted_frames_.empty())
|
||||
return Result<SolidDynamicAcceptedFrameState>::Failure(
|
||||
Failure("No accepted Solid endpoint exists."));
|
||||
return Result<SolidDynamicAcceptedFrameState>::Success(
|
||||
accepted_frames_.back());
|
||||
}
|
||||
const std::vector<SolidDynamicAcceptedFrameState>&
|
||||
SolidDynamicAnalysisState::AcceptedFrames() const noexcept {
|
||||
return accepted_frames_;
|
||||
}
|
||||
const SolidDynamicAcceptedFrameState&
|
||||
SolidDynamicAnalysisState::CommittedState() const {
|
||||
if (accepted_frames_.empty())
|
||||
throw std::logic_error("No accepted Solid endpoint.");
|
||||
return accepted_frames_.back();
|
||||
}
|
||||
bool SolidDynamicAnalysisState::IsInitialized() const noexcept {
|
||||
return initialized_;
|
||||
}
|
||||
void SolidDynamicAnalysisState::RecordIteration(
|
||||
SolidDynamicIterationRecord record) {
|
||||
iteration_records_.push_back(std::move(record));
|
||||
}
|
||||
const std::vector<SolidDynamicIterationRecord>&
|
||||
SolidDynamicAnalysisState::IterationRecords() const noexcept {
|
||||
return iteration_records_;
|
||||
}
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,870 @@
|
||||
#include "fesa/analysis/solid_hht_dynamic_procedure.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/hht_time_integration.h"
|
||||
#include "fesa/assembly/load_assembler.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/constraints/essential_constraint_policy.h"
|
||||
#include "fesa/elements/element_factory.h"
|
||||
#include "fesa/elements/hex8_element.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
#include "fesa/solvers/linear/linear_solver.h"
|
||||
#include "fesa/solvers/linear/tangent_solver.h"
|
||||
#include "solid_hht_force_identity.h"
|
||||
#include "solid_hht_high_low.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
using math_internal::Abs;
|
||||
using math_internal::Add;
|
||||
using math_internal::Compare;
|
||||
using math_internal::Divide;
|
||||
using math_internal::HighLow;
|
||||
using math_internal::IsFinite;
|
||||
using math_internal::IsNormalized;
|
||||
using math_internal::Multiply;
|
||||
using math_internal::Negate;
|
||||
using math_internal::Rounded;
|
||||
using math_internal::Subtract;
|
||||
|
||||
HighLow Pair(const Vector& high, const Vector& low, std::size_t i) {
|
||||
return {high[i], low[i]};
|
||||
}
|
||||
void Store(HighLow value, double* high, double* low) {
|
||||
*high = value.high;
|
||||
*low = value.low;
|
||||
}
|
||||
void Store(HighLow value, Vector* high, Vector* low, std::size_t i) {
|
||||
Store(value, &(*high)[i], &(*low)[i]);
|
||||
}
|
||||
bool Valid(HighLow value) { return IsFinite(value) && IsNormalized(value); }
|
||||
HighLow Maximum(HighLow left, HighLow right) {
|
||||
if (!Valid(left) || !Valid(right)) return math_internal::InvalidHighLow();
|
||||
return Compare(left, right) >= 0 ? left : right;
|
||||
}
|
||||
void InitializeLowVectors(SolidDynamicAcceptedFrameState* f, std::size_t size) {
|
||||
for (auto* v : {&f->low.displacement, &f->low.velocity, &f->low.acceleration,
|
||||
&f->low.algorithmic_acceleration, &f->low.inertia_force,
|
||||
&f->low.internal_force, &f->low.external_force,
|
||||
&f->low.residual, &f->low.reaction, &f->low.residual_scale})
|
||||
*v = Vector(size);
|
||||
}
|
||||
struct PairedTangent {
|
||||
std::vector<HighLow> values;
|
||||
};
|
||||
/// @brief Reports source-context procedure failure without committing a trial.
|
||||
Status Failure(const Domain& domain, const char* code,
|
||||
const std::string& message) {
|
||||
return Status::Failure(
|
||||
FailureCategory::kSolver,
|
||||
{{Severity::kError, code,
|
||||
domain.Dynamic() == nullptr ? SourceLocation{}
|
||||
: domain.Dynamic()->dynamic_location,
|
||||
"DYNAMIC",
|
||||
domain.Dynamic() == nullptr ? ""
|
||||
: domain.Dynamic()->source_step_name_raw,
|
||||
message}});
|
||||
}
|
||||
Status CheckArithmeticEnvironment(const Domain& domain) {
|
||||
return math_internal::ArithmeticEnvironmentSupported()
|
||||
? Status::Ok()
|
||||
: Failure(domain, "solid-dynamic-arithmetic-environment",
|
||||
"Paired HHT requires nearest rounding and gradual "
|
||||
"underflow, including after backend callbacks.");
|
||||
}
|
||||
/// @brief Owns immutable kernels and the stable full-space translation scatter.
|
||||
struct RuntimeSolid {
|
||||
std::unique_ptr<Element> owner;
|
||||
const Hex8Element* solid;
|
||||
std::vector<std::size_t> scatter;
|
||||
};
|
||||
/// @brief Creates checked kernels in declaration order, with no mutable element
|
||||
/// state.
|
||||
Result<std::vector<RuntimeSolid>> BuildSolids(const Domain& domain,
|
||||
const DofManager& dofs) {
|
||||
std::vector<RuntimeSolid> solids;
|
||||
ElementFactory factory;
|
||||
for (std::size_t e = 0; e < domain.Elements().Size(); ++e) {
|
||||
auto owner = factory.Create(domain.Elements()[e], domain);
|
||||
if (!owner.HasValue())
|
||||
return Result<std::vector<RuntimeSolid>>::Failure(owner.GetStatus());
|
||||
const auto* solid = dynamic_cast<const Hex8Element*>(owner.Value().get());
|
||||
if (solid == nullptr)
|
||||
return Result<std::vector<RuntimeSolid>>::Failure(
|
||||
Failure(domain, "solid-dynamic-element-family",
|
||||
"HHT requires exclusively Hex8 kernels."));
|
||||
auto scatter = dofs.ElementScatter(solid->DofLayout());
|
||||
if (!scatter.HasValue())
|
||||
return Result<std::vector<RuntimeSolid>>::Failure(scatter.GetStatus());
|
||||
solids.push_back(
|
||||
{std::move(owner.Value()), solid, std::move(scatter.Value())});
|
||||
}
|
||||
return Result<std::vector<RuntimeSolid>>::Success(std::move(solids));
|
||||
}
|
||||
/// @brief Preserves all 576 structural entries in stable element/local order.
|
||||
void AppendMatrix(const Matrix& local, const RuntimeSolid& solid, std::size_t e,
|
||||
std::vector<CooContribution>* coo) {
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
for (std::size_t j = 0; j < 24; ++j)
|
||||
coo->push_back(
|
||||
{solid.scatter[i], solid.scatter[j], local(i, j), e, 24 * i + j});
|
||||
}
|
||||
/// @brief Assembles constant reference consistent mass once without lumping.
|
||||
Result<SparseMatrix> AssembleMass(const std::vector<RuntimeSolid>& solids,
|
||||
const DofManager& dofs) {
|
||||
std::vector<CooContribution> coo;
|
||||
coo.reserve(solids.size() * 576);
|
||||
for (std::size_t e = 0; e < solids.size(); ++e) {
|
||||
auto mass = solids[e].solid->ComputeMass();
|
||||
if (!mass.HasValue())
|
||||
return Result<SparseMatrix>::Failure(mass.GetStatus());
|
||||
AppendMatrix(mass.Value().values, solids[e], e, &coo);
|
||||
}
|
||||
return SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(),
|
||||
std::move(coo), dofs.GetSparsePattern());
|
||||
}
|
||||
/// @brief Evaluates independent local trials, then deterministically reduces
|
||||
/// recovery.
|
||||
Result<PairedTangent> EvaluateSolids(const Domain& domain,
|
||||
const std::vector<RuntimeSolid>& solids,
|
||||
const DofManager& dofs,
|
||||
const ParallelFor& parallel,
|
||||
SolidDynamicAcceptedFrameState* f) {
|
||||
const auto environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return Result<PairedTangent>::Failure(environment);
|
||||
if (!detail::ValidSolidHhtPair(f->displacement, f->low.displacement,
|
||||
dofs.FullDofCount()))
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-invalid-paired-state",
|
||||
"Local evaluation requires complete normalized pairs."));
|
||||
std::vector<std::optional<Result<Hex8HighLowResponse>>> responses(
|
||||
solids.size());
|
||||
parallel.Execute(solids.size(), [&](std::size_t e) {
|
||||
Vector local(24), low(24);
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
local[i] = f->displacement[solids[e].scatter[i]];
|
||||
low[i] = f->low.displacement[solids[e].scatter[i]];
|
||||
}
|
||||
responses[e] = domain.Dynamic()->nlgeom
|
||||
? solids[e].solid->EvaluateNonlinearHighLow(local, low)
|
||||
: solids[e].solid->EvaluateLinearHighLow(local, low);
|
||||
});
|
||||
const auto reduction_environment = CheckArithmeticEnvironment(domain);
|
||||
if (!reduction_environment.IsOk())
|
||||
return Result<PairedTangent>::Failure(reduction_environment);
|
||||
const auto& pattern = dofs.GetSparsePattern();
|
||||
PairedTangent tangent;
|
||||
tangent.values.resize(pattern.column_indices.size());
|
||||
f->internal_force = Vector(dofs.FullDofCount());
|
||||
f->low.internal_force = Vector(dofs.FullDofCount());
|
||||
f->strain_energy = 0;
|
||||
f->low.strain_energy = 0;
|
||||
f->solid_results.clear();
|
||||
for (std::size_t e = 0; e < solids.size(); ++e) {
|
||||
if (!responses[e])
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-local-execution",
|
||||
"Local worker did not evaluate its index."));
|
||||
if (!responses[e]->HasValue())
|
||||
return Result<PairedTangent>::Failure(responses[e]->GetStatus());
|
||||
const auto& r = responses[e]->Value();
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
const auto row = solids[e].scatter[i];
|
||||
const auto force =
|
||||
Add(Pair(f->internal_force, f->low.internal_force, row),
|
||||
Pair(r.high.internal_force, r.internal_force_low, i));
|
||||
if (!Valid(force))
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-nonfinite-assembly",
|
||||
"Paired internal force reduction failed."));
|
||||
Store(force, &f->internal_force, &f->low.internal_force, row);
|
||||
for (std::size_t j = 0; j < 24; ++j) {
|
||||
const auto column = solids[e].scatter[j];
|
||||
const auto begin =
|
||||
pattern.column_indices.begin() + pattern.row_offsets[row];
|
||||
const auto end =
|
||||
pattern.column_indices.begin() + pattern.row_offsets[row + 1];
|
||||
const auto found = std::lower_bound(begin, end, column);
|
||||
if (found == end || *found != column)
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-sparse-pattern",
|
||||
"Local tangent is outside the fixed sparse pattern."));
|
||||
const auto k =
|
||||
static_cast<std::size_t>(found - pattern.column_indices.begin());
|
||||
tangent.values[k] =
|
||||
Add(tangent.values[k],
|
||||
HighLow(r.high.tangent(i, j), r.tangent_low(i, j)));
|
||||
if (!Valid(tangent.values[k]))
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-nonfinite-assembly",
|
||||
"Paired tangent reduction failed."));
|
||||
}
|
||||
}
|
||||
const auto energy = Add(HighLow(f->strain_energy, f->low.strain_energy),
|
||||
HighLow(r.high.strain_energy, r.strain_energy_low));
|
||||
if (!Valid(energy))
|
||||
return Result<PairedTangent>::Failure(
|
||||
Failure(domain, "solid-dynamic-nonfinite-assembly",
|
||||
"Paired strain energy reduction failed."));
|
||||
Store(energy, &f->strain_energy, &f->low.strain_energy);
|
||||
f->solid_results.insert(f->solid_results.end(), r.high.points.begin(),
|
||||
r.high.points.end());
|
||||
}
|
||||
return Result<PairedTangent>::Success(std::move(tangent));
|
||||
}
|
||||
/// @brief Returns the finite positive reference nodal bounding-box diagonal.
|
||||
Result<double> ReferenceLength(const Domain& domain) {
|
||||
auto lo = domain.Nodes().front().coordinates;
|
||||
auto hi = lo;
|
||||
for (const auto& node : domain.Nodes())
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
lo[c] = (std::min)(lo[c], node.coordinates[c]);
|
||||
hi[c] = (std::max)(hi[c], node.coordinates[c]);
|
||||
}
|
||||
const double length =
|
||||
std::hypot(std::hypot(hi[0] - lo[0], hi[1] - lo[1]), hi[2] - lo[2]);
|
||||
if (!std::isfinite(length) || length <= 0)
|
||||
return Result<double>::Failure(
|
||||
Failure(domain, "solid-dynamic-reference-length",
|
||||
"Reference length is not finite positive."));
|
||||
return Result<double>::Success(length);
|
||||
}
|
||||
/// @brief Forms M/(beta*h²)+(1+alpha)*Kt in the unchanged full sparse pattern.
|
||||
Result<SparseMatrix> EffectiveTangent(const SparseMatrix& m,
|
||||
const PairedTangent& k,
|
||||
const DofManager& dofs, double h,
|
||||
const HhtParameters& p) {
|
||||
std::vector<CooContribution> coo;
|
||||
coo.reserve(m.Values().size());
|
||||
const auto denominator =
|
||||
Multiply(Multiply(HighLow(p.beta), HighLow(h)), HighLow(h));
|
||||
const auto coefficient = Add(HighLow(1), HighLow(p.alpha));
|
||||
for (std::size_t i = 0; i < m.Rows(); ++i)
|
||||
for (std::size_t j = m.RowOffsets()[i]; j < m.RowOffsets()[i + 1]; ++j) {
|
||||
const auto value = Add(Divide(HighLow(m.Values()[j]), denominator),
|
||||
Multiply(coefficient, k.values[j]));
|
||||
if (!Valid(value))
|
||||
return Result<SparseMatrix>::Failure(
|
||||
detail::SolidHhtPairedKinematicsFailure());
|
||||
coo.push_back({i, m.ColumnIndices()[j], Rounded(value), 0, j});
|
||||
}
|
||||
return SparseMatrix::FromCoo(m.Rows(), m.Columns(), std::move(coo),
|
||||
dofs.GetSparsePattern());
|
||||
}
|
||||
|
||||
using Paired3 = std::array<HighLow, 3>;
|
||||
void CrossAdd(const Paired3& x, const Paired3& force, Paired3* sum) {
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
const auto j = (c + 1) % 3;
|
||||
const auto k = (c + 2) % 3;
|
||||
(*sum)[c] = Add((*sum)[c], Subtract(Multiply(x[j], force[k]),
|
||||
Multiply(x[k], force[j])));
|
||||
}
|
||||
}
|
||||
bool Store3(const Paired3& values, std::array<double, 3>* high,
|
||||
std::array<double, 3>* low) {
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
if (!Valid(values[c])) return false;
|
||||
Store(values[c], &(*high)[c], &(*low)[c]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @brief Recovers full physical endpoint quantities before any publication or
|
||||
/// commit. The constant Float64 mass coefficients are lifted exactly once.
|
||||
Status RecoverEndpoint(const Domain& domain, const DofManager& dofs,
|
||||
const SparseMatrix& mass,
|
||||
const SolidDynamicAcceptedFrameState* old,
|
||||
HighLow initial_energy,
|
||||
SolidDynamicAcceptedFrameState* f,
|
||||
SolidDynamicIterationEvidence* e) {
|
||||
auto inertia = detail::MultiplySolidHhtMassHighLow(mass, f->acceleration,
|
||||
f->low.acceleration);
|
||||
auto momentum =
|
||||
detail::MultiplySolidHhtMassHighLow(mass, f->velocity, f->low.velocity);
|
||||
if (!inertia.HasValue()) return inertia.GetStatus();
|
||||
if (!momentum.HasValue()) return momentum.GetStatus();
|
||||
f->inertia_force = std::move(inertia.Value().high);
|
||||
f->low.inertia_force = std::move(inertia.Value().low);
|
||||
const auto size = dofs.FullDofCount();
|
||||
for (auto* v : {&f->residual, &f->low.residual, &f->reaction,
|
||||
&f->low.reaction, &f->residual_scale, &f->low.residual_scale})
|
||||
*v = Vector(size);
|
||||
HighLow numerator, scale_maximum, metric;
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
const auto ma = Pair(f->inertia_force, f->low.inertia_force, i);
|
||||
const auto internal = Pair(f->internal_force, f->low.internal_force, i);
|
||||
const auto external = Pair(f->external_force, f->low.external_force, i);
|
||||
const auto residual = Subtract(Add(ma, internal), external);
|
||||
const auto scale = Add(Add(Abs(ma), Abs(internal)), Abs(external));
|
||||
if (!Valid(residual) || !Valid(scale))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-residual",
|
||||
"Physical paired force arithmetic failed.");
|
||||
Store(residual, &f->residual, &f->low.residual, i);
|
||||
Store(scale, &f->residual_scale, &f->low.residual_scale, i);
|
||||
}
|
||||
for (auto i : dofs.ConstrainedDofs()) {
|
||||
f->reaction[i] = f->residual[i];
|
||||
f->low.reaction[i] = f->low.residual[i];
|
||||
}
|
||||
for (auto i : dofs.FreeDofs()) {
|
||||
const auto residual = Pair(f->residual, f->low.residual, i);
|
||||
const auto scale = Pair(f->residual_scale, f->low.residual_scale, i);
|
||||
numerator = Maximum(numerator, Abs(residual));
|
||||
scale_maximum = Maximum(scale_maximum, scale);
|
||||
if (Compare(scale, HighLow(0)) == 0) {
|
||||
e->zero_scale_exact =
|
||||
e->zero_scale_exact && Compare(residual, HighLow(0)) == 0;
|
||||
} else {
|
||||
const auto ratio = Divide(Abs(residual), scale);
|
||||
if (!Valid(ratio))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-residual",
|
||||
"Physical paired force ratio is unsupported.");
|
||||
if (Compare(ratio, metric) > 0) {
|
||||
metric = ratio;
|
||||
e->worst_full_dof = i;
|
||||
e->worst_force_terms = {
|
||||
Rounded(residual),
|
||||
Rounded(Pair(f->inertia_force, f->low.inertia_force, i)),
|
||||
Rounded(Pair(f->internal_force, f->low.internal_force, i)),
|
||||
Rounded(Pair(f->external_force, f->low.external_force, i)),
|
||||
Rounded(scale)};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Valid(numerator) || !Valid(scale_maximum) || !Valid(metric))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-residual",
|
||||
"Physical paired evidence is nonfinite.");
|
||||
e->residual_numerator = Rounded(numerator);
|
||||
e->residual_scale = Rounded(scale_maximum);
|
||||
e->residual_metric = Rounded(metric);
|
||||
e->residual_pass =
|
||||
e->zero_scale_exact && Compare(metric, HighLow(1e-10)) <= 0;
|
||||
|
||||
HighLow kinetic;
|
||||
HighLow work = old == nullptr
|
||||
? HighLow(0)
|
||||
: HighLow(old->external_work, old->low.external_work);
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
kinetic =
|
||||
Add(kinetic,
|
||||
Multiply(Pair(f->velocity, f->low.velocity, i),
|
||||
Pair(momentum.Value().high, momentum.Value().low, i)));
|
||||
if (old != nullptr) {
|
||||
const auto average =
|
||||
Multiply(HighLow(.5),
|
||||
Add(Pair(old->external_force, old->low.external_force, i),
|
||||
Pair(f->external_force, f->low.external_force, i)));
|
||||
const auto delta =
|
||||
Subtract(Pair(f->displacement, f->low.displacement, i),
|
||||
Pair(old->displacement, old->low.displacement, i));
|
||||
work = Add(work, Multiply(average, delta));
|
||||
}
|
||||
}
|
||||
kinetic = Multiply(HighLow(.5), kinetic);
|
||||
const auto error = Subtract(
|
||||
Subtract(Add(kinetic, HighLow(f->strain_energy, f->low.strain_energy)),
|
||||
initial_energy),
|
||||
work);
|
||||
if (!Valid(kinetic) || !Valid(work) || !Valid(error))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-energy",
|
||||
"Paired energy or endpoint work is unsupported.");
|
||||
Store(kinetic, &f->kinetic_energy, &f->low.kinetic_energy);
|
||||
Store(work, &f->external_work, &f->low.external_work);
|
||||
Store(error, &f->energy_balance_error, &f->low.energy_balance_error);
|
||||
|
||||
Paired3 linear{}, angular{}, linear_rate{}, angular_rate{}, applied_moment{},
|
||||
linear_equilibrium{}, current_internal_moment{}, reference_moment{},
|
||||
free_residual_moment{};
|
||||
for (std::size_t n = 0; n < domain.Nodes().size(); ++n) {
|
||||
Paired3 x{}, reference{}, mv{}, ma{}, applied{}, internal{},
|
||||
free_residual{}, u{};
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
const auto i = 3 * n + c;
|
||||
reference[c] = HighLow(domain.Nodes()[n].coordinates[c]);
|
||||
u[c] = Pair(f->displacement, f->low.displacement, i);
|
||||
x[c] = Add(reference[c], u[c]);
|
||||
mv[c] = Pair(momentum.Value().high, momentum.Value().low, i);
|
||||
ma[c] = Pair(f->inertia_force, f->low.inertia_force, i);
|
||||
internal[c] = Pair(f->internal_force, f->low.internal_force, i);
|
||||
applied[c] = Add(Pair(f->external_force, f->low.external_force, i),
|
||||
Pair(f->reaction, f->low.reaction, i));
|
||||
free_residual[c] = Subtract(Pair(f->residual, f->low.residual, i),
|
||||
Pair(f->reaction, f->low.reaction, i));
|
||||
linear[c] = Add(linear[c], mv[c]);
|
||||
linear_rate[c] = Add(linear_rate[c], ma[c]);
|
||||
linear_equilibrium[c] =
|
||||
Add(linear_equilibrium[c], Subtract(applied[c], ma[c]));
|
||||
}
|
||||
CrossAdd(x, mv, &angular);
|
||||
CrossAdd(x, ma, &angular_rate);
|
||||
CrossAdd(x, applied, &applied_moment);
|
||||
CrossAdd(u, internal, ¤t_internal_moment);
|
||||
CrossAdd(reference, internal, &reference_moment);
|
||||
CrossAdd(x, free_residual, &free_residual_moment);
|
||||
}
|
||||
if (!Store3(linear, &f->linear_momentum, &f->low.linear_momentum) ||
|
||||
!Store3(angular, &f->angular_momentum, &f->low.angular_momentum) ||
|
||||
!Store3(linear_rate, &f->linear_momentum_rate,
|
||||
&f->low.linear_momentum_rate) ||
|
||||
!Store3(angular_rate, &f->angular_momentum_rate,
|
||||
&f->low.angular_momentum_rate) ||
|
||||
!Store3(current_internal_moment,
|
||||
&f->linearization_current_internal_moment,
|
||||
&f->low.linearization_current_internal_moment) ||
|
||||
!Store3(reference_moment, &f->reference_internal_moment,
|
||||
&f->low.reference_internal_moment) ||
|
||||
!Store3(free_residual_moment, &f->current_free_residual_moment,
|
||||
&f->low.current_free_residual_moment))
|
||||
return Failure(
|
||||
domain, "solid-dynamic-nonfinite-momentum",
|
||||
"Paired momentum or current/reference moment is unsupported.");
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
const auto moment = Subtract(applied_moment[c], angular_rate[c]);
|
||||
if (!Valid(linear_equilibrium[c]) || !Valid(moment))
|
||||
return Failure(domain, "solid-dynamic-nonfinite-equilibrium",
|
||||
"Paired global equilibrium is unsupported.");
|
||||
Store(linear_equilibrium[c], &f->global_equilibrium[c],
|
||||
&f->low.global_equilibrium[c]);
|
||||
Store(moment, &f->global_equilibrium[3 + c],
|
||||
&f->low.global_equilibrium[3 + c]);
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
/// @brief Uses full paired same-unit uncancelled formula terms.
|
||||
HighLow UpdateMetric(HighLow left, std::initializer_list<HighLow> terms) {
|
||||
HighLow sum;
|
||||
auto scale = Abs(left);
|
||||
for (const auto x : terms) {
|
||||
sum = Add(sum, x);
|
||||
scale = Add(scale, Abs(x));
|
||||
}
|
||||
const auto defect = Abs(Subtract(left, sum));
|
||||
if (!Valid(defect) || !Valid(scale)) return math_internal::InvalidHighLow();
|
||||
if (Compare(scale, HighLow(0)) == 0)
|
||||
return Compare(defect, HighLow(0)) == 0 ? HighLow(0)
|
||||
: math_internal::InvalidHighLow();
|
||||
return Divide(defect, scale);
|
||||
}
|
||||
HighLow CheckUpdates(const SolidDynamicAcceptedFrameState& old,
|
||||
const SolidDynamicAcceptedFrameState& f,
|
||||
const HhtParameters& p, double h) {
|
||||
HighLow metric;
|
||||
const auto step = HighLow(h);
|
||||
const auto hh = Multiply(step, step);
|
||||
for (std::size_t i = 0; i < f.displacement.Size(); ++i) {
|
||||
const auto A =
|
||||
Pair(f.algorithmic_acceleration, f.low.algorithmic_acceleration, i);
|
||||
const auto oldA =
|
||||
Pair(old.algorithmic_acceleration, old.low.algorithmic_acceleration, i);
|
||||
const auto oldv = Pair(old.velocity, old.low.velocity, i);
|
||||
metric = Maximum(
|
||||
metric,
|
||||
UpdateMetric(
|
||||
Pair(f.displacement, f.low.displacement, i),
|
||||
{Pair(old.displacement, old.low.displacement, i),
|
||||
Multiply(step, oldv),
|
||||
Multiply(Multiply(hh, Subtract(HighLow(.5), HighLow(p.beta))),
|
||||
oldA),
|
||||
Multiply(Multiply(hh, HighLow(p.beta)), A)}));
|
||||
metric = Maximum(
|
||||
metric,
|
||||
UpdateMetric(
|
||||
Pair(f.velocity, f.low.velocity, i),
|
||||
{oldv,
|
||||
Multiply(Multiply(step, Subtract(HighLow(1), HighLow(p.gamma))),
|
||||
oldA),
|
||||
Multiply(Multiply(step, HighLow(p.gamma)), A)}));
|
||||
metric = Maximum(
|
||||
metric,
|
||||
UpdateMetric(A, {Multiply(Add(HighLow(1), HighLow(p.alpha)),
|
||||
Pair(f.acceleration, f.low.acceleration, i)),
|
||||
Negate(Multiply(HighLow(p.alpha),
|
||||
Pair(old.acceleration,
|
||||
old.low.acceleration, i)))}));
|
||||
}
|
||||
return metric;
|
||||
}
|
||||
std::array<HighLow, 4> FreshCorrectionMetrics(const Vector& du, double length,
|
||||
const HhtParameters& p) {
|
||||
HighLow q;
|
||||
for (std::size_t i = 0; i < du.Size(); ++i)
|
||||
q = Maximum(q, Divide(Abs(HighLow(du[i])), HighLow(length)));
|
||||
return {
|
||||
q, Divide(Multiply(HighLow(p.gamma), q), HighLow(p.beta)),
|
||||
Divide(q, Multiply(Add(HighLow(1), HighLow(p.alpha)), HighLow(p.beta))),
|
||||
Divide(q, HighLow(p.beta))};
|
||||
}
|
||||
std::array<HighLow, 4> ObservedCorrectionMetrics(
|
||||
const SolidDynamicAcceptedFrameState& before,
|
||||
const SolidDynamicAcceptedFrameState& after, double h, double length) {
|
||||
std::array<HighLow, 4> metric{};
|
||||
const auto hh = Multiply(HighLow(h), HighLow(h));
|
||||
for (std::size_t i = 0; i < after.displacement.Size(); ++i) {
|
||||
metric[0] = Maximum(
|
||||
metric[0],
|
||||
Divide(Abs(Subtract(
|
||||
Pair(after.displacement, after.low.displacement, i),
|
||||
Pair(before.displacement, before.low.displacement, i))),
|
||||
HighLow(length)));
|
||||
metric[1] = Maximum(
|
||||
metric[1],
|
||||
Divide(Multiply(HighLow(h),
|
||||
Abs(Subtract(
|
||||
Pair(after.velocity, after.low.velocity, i),
|
||||
Pair(before.velocity, before.low.velocity, i)))),
|
||||
HighLow(length)));
|
||||
metric[2] = Maximum(
|
||||
metric[2],
|
||||
Divide(Multiply(hh, Abs(Subtract(Pair(after.acceleration,
|
||||
after.low.acceleration, i),
|
||||
Pair(before.acceleration,
|
||||
before.low.acceleration, i)))),
|
||||
HighLow(length)));
|
||||
metric[3] = Maximum(
|
||||
metric[3],
|
||||
Divide(Multiply(hh, Abs(Subtract(
|
||||
Pair(after.algorithmic_acceleration,
|
||||
after.low.algorithmic_acceleration, i),
|
||||
Pair(before.algorithmic_acceleration,
|
||||
before.low.algorithmic_acceleration, i)))),
|
||||
HighLow(length)));
|
||||
}
|
||||
return metric;
|
||||
}
|
||||
bool CorrectionPass(const std::array<HighLow, 4>& metrics) {
|
||||
return std::all_of(metrics.begin(), metrics.end(), [](HighLow x) {
|
||||
return Valid(x) && Compare(x, HighLow(0)) >= 0 &&
|
||||
Compare(x, HighLow(1e-10)) <= 0;
|
||||
});
|
||||
}
|
||||
Status SetRates(const SolidDynamicAcceptedFrameState& old,
|
||||
const detail::SolidHhtPairedPredictor& predictor, double h,
|
||||
const HhtParameters& p,
|
||||
SolidDynamicAcceptedFrameState* candidate) {
|
||||
auto rates = detail::EvaluateSolidHhtHighLowRates(
|
||||
candidate->displacement, candidate->low.displacement, predictor,
|
||||
old.acceleration, old.low.acceleration, h, p);
|
||||
if (!rates.HasValue()) return rates.GetStatus();
|
||||
candidate->velocity = std::move(rates.Value().velocity);
|
||||
candidate->low.velocity = std::move(rates.Value().velocity_low);
|
||||
candidate->acceleration = std::move(rates.Value().acceleration);
|
||||
candidate->low.acceleration = std::move(rates.Value().acceleration_low);
|
||||
candidate->algorithmic_acceleration =
|
||||
std::move(rates.Value().algorithmic_acceleration);
|
||||
candidate->low.algorithmic_acceleration =
|
||||
std::move(rates.Value().algorithmic_acceleration_low);
|
||||
return Status::Ok();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SolidHhtDynamicProcedureExecutor::SolidHhtDynamicProcedureExecutor(
|
||||
const ParallelFor& parallel_for, LinearSolver& mass_solver,
|
||||
TangentSolver& tangent_solver)
|
||||
: parallel_for_(parallel_for),
|
||||
mass_solver_(mass_solver),
|
||||
tangent_solver_(tangent_solver) {}
|
||||
|
||||
Result<SolidDynamicExecutionResult> SolidHhtDynamicProcedureExecutor::Execute(
|
||||
const Domain& domain) const {
|
||||
auto model = AnalysisModel::Create(domain);
|
||||
if (!model.HasValue())
|
||||
return Result<SolidDynamicExecutionResult>::Failure(model.GetStatus());
|
||||
auto dofs = DofManager::Create(model.Value());
|
||||
if (!dofs.HasValue())
|
||||
return Result<SolidDynamicExecutionResult>::Failure(dofs.GetStatus());
|
||||
auto state = SolidDynamicAnalysisState::Create(domain, dofs.Value());
|
||||
if (!state.HasValue())
|
||||
return Result<SolidDynamicExecutionResult>::Failure(state.GetStatus());
|
||||
auto status = ExecuteInto(domain, state.Value());
|
||||
if (!status.IsOk())
|
||||
return Result<SolidDynamicExecutionResult>::Failure(status);
|
||||
return Result<SolidDynamicExecutionResult>::Success(
|
||||
{std::move(dofs.Value()), std::move(state.Value())});
|
||||
}
|
||||
Status SolidHhtDynamicProcedureExecutor::ExecuteInto(
|
||||
const Domain& domain, SolidDynamicAnalysisState& state) const {
|
||||
return ExecuteIntoForVerification(domain, state, -0.05);
|
||||
}
|
||||
Status SolidHhtDynamicProcedureExecutor::ExecuteIntoForVerification(
|
||||
const Domain& domain, SolidDynamicAnalysisState& state,
|
||||
double alpha) const {
|
||||
SolidDynamicIterationRecord record;
|
||||
auto fail = [&](const Status& failure) {
|
||||
record.stage = "failed";
|
||||
record.outcome = "failed";
|
||||
record.failure_code = failure.Diagnostics().empty()
|
||||
? "solid-dynamic-failure"
|
||||
: failure.Diagnostics().front().code;
|
||||
state.RecordIteration(record);
|
||||
return failure;
|
||||
};
|
||||
if (!state.IsInitialized() || !state.AcceptedFrames().empty() ||
|
||||
!domain.IsSolid() || domain.Dynamic() == nullptr)
|
||||
return fail(Failure(
|
||||
domain, "solid-dynamic-invalid-execution",
|
||||
"Execution requires an initialized empty Solid dynamic state."));
|
||||
auto environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
auto model = AnalysisModel::Create(domain);
|
||||
if (!model.HasValue()) return fail(model.GetStatus());
|
||||
auto dofs_result = DofManager::Create(model.Value());
|
||||
if (!dofs_result.HasValue()) return fail(dofs_result.GetStatus());
|
||||
const auto& dofs = dofs_result.Value();
|
||||
auto solids = BuildSolids(domain, dofs);
|
||||
if (!solids.HasValue()) return fail(solids.GetStatus());
|
||||
auto mass = AssembleMass(solids.Value(), dofs);
|
||||
if (!mass.HasValue()) return fail(mass.GetStatus());
|
||||
EssentialConstraintPolicy partition;
|
||||
auto mass_partition = partition.Partition(mass.Value(), dofs);
|
||||
if (!mass_partition.HasValue()) return fail(mass_partition.GetStatus());
|
||||
if (dofs.FreeDofCount() != 0) {
|
||||
const auto status = mass_solver_.Factorize(mass_partition.Value().k_ff);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
}
|
||||
auto parameters = MakeHhtParameters(alpha);
|
||||
if (!parameters.HasValue()) return fail(parameters.GetStatus());
|
||||
const auto p = parameters.Value();
|
||||
auto length = ReferenceLength(domain);
|
||||
if (!length.HasValue()) return fail(length.GetStatus());
|
||||
const double h = domain.Dynamic()->initial_increment;
|
||||
SolidDynamicAcceptedFrameState initial;
|
||||
initial.is_initial = true;
|
||||
for (std::size_t n = 0; n < domain.Nodes().size(); ++n) {
|
||||
initial.nodal_internal_order.push_back(static_cast<EntityIndex>(n));
|
||||
initial.nodal_source_ids.push_back(domain.Nodes()[n].source_id);
|
||||
}
|
||||
for (auto* v : {&initial.displacement, &initial.velocity,
|
||||
&initial.acceleration, &initial.algorithmic_acceleration})
|
||||
*v = Vector(dofs.FullDofCount());
|
||||
InitializeLowVectors(&initial, dofs.FullDofCount());
|
||||
auto force = LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs, 0);
|
||||
if (!force.HasValue()) return fail(force.GetStatus());
|
||||
initial.external_force = std::move(force.Value());
|
||||
auto tangent =
|
||||
EvaluateSolids(domain, solids.Value(), dofs, parallel_for_, &initial);
|
||||
if (!tangent.HasValue()) return fail(tangent.GetStatus());
|
||||
SolidDynamicIterationEvidence evidence;
|
||||
bool initial_converged = false;
|
||||
for (std::uint32_t refinement = 0; refinement <= 100; ++refinement) {
|
||||
initial.algorithmic_acceleration = initial.acceleration;
|
||||
initial.low.algorithmic_acceleration = initial.low.acceleration;
|
||||
evidence = {};
|
||||
evidence.correction_pass = evidence.update_pass = true;
|
||||
auto status = RecoverEndpoint(domain, dofs, mass.Value(), nullptr,
|
||||
HighLow(0), &initial, &evidence);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
if (evidence.residual_pass) {
|
||||
initial_converged = true;
|
||||
break;
|
||||
}
|
||||
record.stage = "initial-mass";
|
||||
record.outcome = "residual-refinement";
|
||||
record.evidence = evidence;
|
||||
record.evidence.iteration_index = refinement;
|
||||
state.RecordIteration(record);
|
||||
if (refinement == 100) break;
|
||||
Vector rhs(dofs.FreeDofCount()), a(dofs.FreeDofCount());
|
||||
for (std::size_t j = 0; j < dofs.FreeDofCount(); ++j) {
|
||||
const auto i = dofs.FreeDofs()[j];
|
||||
rhs[j] = Rounded(Negate(Pair(initial.residual, initial.low.residual, i)));
|
||||
}
|
||||
status = mass_solver_.Solve(rhs, a);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
if (a.Size() != dofs.FreeDofCount())
|
||||
return fail(
|
||||
Failure(domain, "solid-dynamic-invalid-backend-result",
|
||||
"Initial mass solve returned incompatible acceleration."));
|
||||
for (std::size_t j = 0; j < a.Size(); ++j) {
|
||||
if (!std::isfinite(a[j]))
|
||||
return fail(
|
||||
Failure(domain, "solid-dynamic-invalid-backend-result",
|
||||
"Initial mass solve returned nonfinite acceleration."));
|
||||
const auto i = dofs.FreeDofs()[j];
|
||||
const auto value =
|
||||
Add(Pair(initial.acceleration, initial.low.acceleration, i),
|
||||
HighLow(a[j] == 0 ? 0.0 : a[j]));
|
||||
if (!Valid(value)) return fail(detail::SolidHhtPairedKinematicsFailure());
|
||||
Store(value, &initial.acceleration, &initial.low.acceleration, i);
|
||||
}
|
||||
}
|
||||
if (!initial_converged)
|
||||
return fail(Failure(domain, "solid-dynamic-initial-mass-nonconvergence",
|
||||
"Physical initial equilibrium did not converge in100 "
|
||||
"mass refinements."));
|
||||
initial.iterations.push_back(evidence);
|
||||
auto status = state.CommitAcceptedFrame(initial);
|
||||
if (!status.IsOk()) {
|
||||
record.evidence = evidence;
|
||||
return fail(status);
|
||||
}
|
||||
record.stage = "accepted";
|
||||
record.outcome = "initial";
|
||||
record.evidence = evidence;
|
||||
state.RecordIteration(record);
|
||||
const auto initial_energy =
|
||||
Add(HighLow(initial.strain_energy, initial.low.strain_energy),
|
||||
HighLow(initial.kinetic_energy, initial.low.kinetic_energy));
|
||||
for (std::uint32_t increment = 1;
|
||||
increment <= domain.Dynamic()->increment_count; ++increment) {
|
||||
const auto& old = state.CommittedState();
|
||||
record = {};
|
||||
record.increment_index = increment;
|
||||
record.time = static_cast<double>(increment) * h;
|
||||
auto predictor = detail::PredictSolidHhtHighLow(old, h, p);
|
||||
if (!predictor.HasValue()) return fail(predictor.GetStatus());
|
||||
auto candidate = old;
|
||||
candidate.frame_index = candidate.increment_index = increment;
|
||||
candidate.iteration_count = 0;
|
||||
candidate.time = record.time;
|
||||
candidate.is_initial = false;
|
||||
candidate.iterations.clear();
|
||||
// Begin from certified accepted geometry, not an unchecked predictor.
|
||||
force = LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs,
|
||||
candidate.time);
|
||||
if (!force.HasValue()) return fail(force.GetStatus());
|
||||
candidate.external_force = std::move(force.Value());
|
||||
candidate.low.external_force = Vector(dofs.FullDofCount());
|
||||
std::array<HighLow, 4> correction{}, fresh_correction{};
|
||||
bool accepted = false;
|
||||
for (std::uint32_t iteration = 0; iteration <= 100; ++iteration) {
|
||||
record.stage = "evaluation";
|
||||
record.outcome = "paired-candidate";
|
||||
record.evidence = {};
|
||||
record.evidence.iteration_index = iteration;
|
||||
status = SetRates(old, predictor.Value(), h, p, &candidate);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
tangent = EvaluateSolids(domain, solids.Value(), dofs, parallel_for_,
|
||||
&candidate);
|
||||
if (!tangent.HasValue()) return fail(tangent.GetStatus());
|
||||
status = RecoverEndpoint(domain, dofs, mass.Value(), &old, initial_energy,
|
||||
&candidate, &record.evidence);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
auto MA = detail::MultiplySolidHhtMassHighLow(
|
||||
mass.Value(), candidate.algorithmic_acceleration,
|
||||
candidate.low.algorithmic_acceleration);
|
||||
if (!MA.HasValue()) return fail(MA.GetStatus());
|
||||
Vector rhs(dofs.FreeDofCount());
|
||||
HighLow identity_metric, original_norm, solved_norm, old_norm;
|
||||
for (std::size_t j = 0; j < dofs.FreeDofCount(); ++j) {
|
||||
const auto i = dofs.FreeDofs()[j];
|
||||
detail::SolidHhtPairedForceIdentity identity;
|
||||
if (!detail::EvaluateSolidHhtHighLowForceIdentity(
|
||||
mass.Value(), i, Pair(MA.Value().high, MA.Value().low, i),
|
||||
candidate, old, p.alpha, &identity))
|
||||
return fail(
|
||||
Failure(domain, "solid-dynamic-nonfinite-residual",
|
||||
"Paired weighted force arithmetic is unsupported."));
|
||||
rhs[j] = Rounded(Negate(identity.solved));
|
||||
identity_metric = Maximum(identity_metric, identity.metric);
|
||||
original_norm = Maximum(original_norm, Abs(identity.original));
|
||||
solved_norm = Maximum(solved_norm, Abs(identity.solved));
|
||||
old_norm =
|
||||
Maximum(old_norm, Abs(Pair(old.residual, old.low.residual, i)));
|
||||
if (i == record.evidence.worst_full_dof)
|
||||
record.evidence.worst_weighted_terms = {
|
||||
Rounded(identity.original), Rounded(identity.solved),
|
||||
Rounded(Pair(old.residual, old.low.residual, i)),
|
||||
Rounded(identity.scale)};
|
||||
}
|
||||
if (!Valid(identity_metric) || !Valid(original_norm) ||
|
||||
!Valid(solved_norm) || !Valid(old_norm) ||
|
||||
Compare(identity_metric, HighLow(1e-12)) > 0)
|
||||
return fail(Failure(domain,
|
||||
"solid-dynamic-weighted-algebra-inconsistency",
|
||||
"Paired original and solved forces disagree beyond "
|
||||
"their uncancelled scale."));
|
||||
record.evidence.weighted_equivalence_metric = Rounded(identity_metric);
|
||||
record.evidence.weighted_residual_numerator = Rounded(original_norm);
|
||||
record.evidence.recombined_residual_numerator = Rounded(solved_norm);
|
||||
record.evidence.old_residual_numerator = Rounded(old_norm);
|
||||
for (std::size_t c = 0; c < 4; ++c) {
|
||||
if (!Valid(correction[c]) || !Valid(fresh_correction[c]))
|
||||
return fail(detail::SolidHhtPairedKinematicsFailure());
|
||||
record.evidence.correction_metrics[c] = Rounded(correction[c]);
|
||||
}
|
||||
record.evidence.correction_pass =
|
||||
CorrectionPass(correction) && CorrectionPass(fresh_correction);
|
||||
const auto update = CheckUpdates(old, candidate, p, h);
|
||||
if (!Valid(update))
|
||||
return fail(detail::SolidHhtPairedKinematicsFailure());
|
||||
record.evidence.update_metric = Rounded(update);
|
||||
record.evidence.update_pass = Compare(update, HighLow(1e-12)) <= 0;
|
||||
candidate.iterations.push_back(record.evidence);
|
||||
candidate.iteration_count = iteration;
|
||||
if (record.evidence.residual_pass && record.evidence.correction_pass &&
|
||||
record.evidence.update_pass) {
|
||||
status = state.CommitAcceptedFrame(candidate);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
record.stage = "accepted";
|
||||
record.outcome = "increment";
|
||||
state.RecordIteration(record);
|
||||
accepted = true;
|
||||
break;
|
||||
}
|
||||
state.RecordIteration(record);
|
||||
if (iteration == 100)
|
||||
return fail(Failure(
|
||||
domain, "solid-dynamic-nonconvergence",
|
||||
"Paired HHT endpoint did not converge in100 Newton solves."));
|
||||
auto effective =
|
||||
EffectiveTangent(mass.Value(), tangent.Value(), dofs, h, p);
|
||||
if (!effective.HasValue()) return fail(effective.GetStatus());
|
||||
auto reduced = partition.Partition(effective.Value(), dofs);
|
||||
if (!reduced.HasValue()) return fail(reduced.GetStatus());
|
||||
Vector du(dofs.FreeDofCount());
|
||||
if (dofs.FreeDofCount() != 0) {
|
||||
status = tangent_solver_.FactorizeFresh(reduced.Value().k_ff);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
status = tangent_solver_.SolveCorrection(rhs, du);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
environment = CheckArithmeticEnvironment(domain);
|
||||
if (!environment.IsOk()) return fail(environment);
|
||||
}
|
||||
if (du.Size() != dofs.FreeDofCount())
|
||||
return fail(Failure(domain, "solid-dynamic-invalid-backend-result",
|
||||
"Tangent solve returned incompatible correction."));
|
||||
for (std::size_t j = 0; j < du.Size(); ++j)
|
||||
if (!std::isfinite(du[j]))
|
||||
return fail(Failure(domain, "solid-dynamic-invalid-backend-result",
|
||||
"Tangent solve returned nonfinite correction."));
|
||||
fresh_correction = FreshCorrectionMetrics(du, length.Value(), p);
|
||||
const auto before = candidate;
|
||||
for (std::size_t j = 0; j < du.Size(); ++j) {
|
||||
const auto i = dofs.FreeDofs()[j];
|
||||
const auto value =
|
||||
Add(Pair(candidate.displacement, candidate.low.displacement, i),
|
||||
HighLow(du[j] == 0 ? 0.0 : du[j]));
|
||||
if (!Valid(value))
|
||||
return fail(detail::SolidHhtPairedKinematicsFailure());
|
||||
Store(value, &candidate.displacement, &candidate.low.displacement, i);
|
||||
}
|
||||
status = SetRates(old, predictor.Value(), h, p, &candidate);
|
||||
if (!status.IsOk()) return fail(status);
|
||||
correction =
|
||||
ObservedCorrectionMetrics(before, candidate, h, length.Value());
|
||||
}
|
||||
if (!accepted)
|
||||
return fail(Failure(domain, "solid-dynamic-nonconvergence",
|
||||
"No complete paired endpoint was accepted."));
|
||||
}
|
||||
return Status::Ok();
|
||||
}
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,182 @@
|
||||
#ifndef FESA_ANALYSIS_SOLID_HHT_FORCE_IDENTITY_H_
|
||||
#define FESA_ANALYSIS_SOLID_HHT_FORCE_IDENTITY_H_
|
||||
|
||||
#include <cmath>
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
#include "solid_hht_high_low.h"
|
||||
|
||||
namespace fesa::detail {
|
||||
struct SolidHhtForceIdentity {
|
||||
double original{0};
|
||||
double solved{0};
|
||||
double scale{0};
|
||||
double defect{0};
|
||||
double metric{0};
|
||||
};
|
||||
|
||||
struct SolidHhtPairedForceIdentity {
|
||||
math_internal::HighLow original;
|
||||
math_internal::HighLow solved;
|
||||
math_internal::HighLow scale;
|
||||
math_internal::HighLow defect;
|
||||
math_internal::HighLow metric;
|
||||
};
|
||||
|
||||
/// @brief Keeps both physical parts and the actual old residual in the approved
|
||||
/// weighted identity. Its scale includes each uncancelled mass product.
|
||||
inline bool EvaluateSolidHhtHighLowForceIdentity(
|
||||
const SparseMatrix& mass, std::size_t row,
|
||||
math_internal::HighLow algorithmic_inertia,
|
||||
const SolidDynamicAcceptedFrameState& next,
|
||||
const SolidDynamicAcceptedFrameState& old, double alpha,
|
||||
SolidHhtPairedForceIdentity* result) {
|
||||
using namespace math_internal;
|
||||
if (!ArithmeticEnvironmentSupported() || row >= mass.Rows() ||
|
||||
!IsFinite(algorithmic_inertia) || !IsNormalized(algorithmic_inertia) ||
|
||||
!IsFinite(HighLow(alpha)))
|
||||
return false;
|
||||
for (const auto* frame : {&next, &old})
|
||||
if (frame->acceleration.Size() != mass.Columns() ||
|
||||
frame->low.acceleration.Size() != mass.Columns() ||
|
||||
frame->algorithmic_acceleration.Size() != mass.Columns() ||
|
||||
frame->low.algorithmic_acceleration.Size() != mass.Columns() ||
|
||||
frame->internal_force.Size() != mass.Rows() ||
|
||||
frame->low.internal_force.Size() != mass.Rows() ||
|
||||
frame->external_force.Size() != mass.Rows() ||
|
||||
frame->low.external_force.Size() != mass.Rows() ||
|
||||
frame->residual.Size() != mass.Rows() ||
|
||||
frame->low.residual.Size() != mass.Rows())
|
||||
return false;
|
||||
const HighLow a(alpha);
|
||||
const auto c = Add(HighLow(1), a);
|
||||
const HighLow f(next.internal_force[row], next.low.internal_force[row]);
|
||||
const HighLow F(next.external_force[row], next.low.external_force[row]);
|
||||
const HighLow old_f(old.internal_force[row], old.low.internal_force[row]);
|
||||
const HighLow old_F(old.external_force[row], old.low.external_force[row]);
|
||||
const HighLow r(next.residual[row], next.low.residual[row]);
|
||||
const HighLow old_r(old.residual[row], old.low.residual[row]);
|
||||
result->original =
|
||||
Subtract(Add(algorithmic_inertia, Multiply(c, Subtract(f, F))),
|
||||
Multiply(a, Subtract(old_f, old_F)));
|
||||
result->solved = Subtract(Multiply(c, r), Multiply(a, old_r));
|
||||
HighLow mass_scale;
|
||||
HighLow relation_action;
|
||||
for (std::size_t k = mass.RowOffsets()[row]; k < mass.RowOffsets()[row + 1];
|
||||
++k) {
|
||||
const auto j = mass.ColumnIndices()[k];
|
||||
const HighLow coefficient(mass.Values()[k]);
|
||||
const HighLow A(next.algorithmic_acceleration[j],
|
||||
next.low.algorithmic_acceleration[j]);
|
||||
const HighLow physical_a(next.acceleration[j], next.low.acceleration[j]);
|
||||
const HighLow old_a(old.acceleration[j], old.low.acceleration[j]);
|
||||
const auto relation =
|
||||
Add(Subtract(A, Multiply(c, physical_a)), Multiply(a, old_a));
|
||||
mass_scale =
|
||||
Add(mass_scale,
|
||||
Add(Add(Abs(Multiply(coefficient, A)),
|
||||
Multiply(c, Abs(Multiply(coefficient, physical_a)))),
|
||||
Multiply(Abs(a), Abs(Multiply(coefficient, old_a)))));
|
||||
relation_action =
|
||||
Add(relation_action, Abs(Multiply(coefficient, relation)));
|
||||
if (!IsFinite(mass_scale) || !IsFinite(relation_action)) return false;
|
||||
}
|
||||
result->scale = Add(
|
||||
Add(Add(mass_scale, relation_action), Multiply(c, Add(Abs(f), Abs(F)))),
|
||||
Add(Add(Multiply(Abs(a), Add(Abs(old_f), Abs(old_F))),
|
||||
Multiply(c, Abs(r))),
|
||||
Abs(Multiply(a, old_r))));
|
||||
result->defect = Abs(Subtract(result->original, result->solved));
|
||||
if (!IsFinite(result->original) || !IsFinite(result->solved) ||
|
||||
!IsFinite(result->scale) || !IsFinite(result->defect))
|
||||
return false;
|
||||
if (Compare(result->scale, HighLow(0)) == 0) {
|
||||
if (Compare(result->defect, HighLow(0)) != 0) return false;
|
||||
result->metric = HighLow(0);
|
||||
} else {
|
||||
result->metric = Divide(result->defect, result->scale);
|
||||
}
|
||||
return IsFinite(result->metric) && IsNormalized(result->metric);
|
||||
}
|
||||
|
||||
/// @brief Evaluates one original/equivalent HHT row, retaining old equilibrium
|
||||
/// error. Checks each scalar before it can enter an evidence maximum.
|
||||
inline bool EvaluateSolidHhtForceIdentity(
|
||||
const SparseMatrix& mass, std::size_t row, double algorithmic_inertia,
|
||||
const SolidDynamicAcceptedFrameState& next,
|
||||
const SolidDynamicAcceptedFrameState& old, double alpha,
|
||||
SolidHhtForceIdentity* result) {
|
||||
for (double x :
|
||||
{alpha, algorithmic_inertia, next.internal_force[row],
|
||||
next.external_force[row], old.internal_force[row],
|
||||
old.external_force[row], next.residual[row], old.residual[row]})
|
||||
if (!std::isfinite(x)) return false;
|
||||
result->original =
|
||||
algorithmic_inertia +
|
||||
(1 + alpha) * (next.internal_force[row] - next.external_force[row]) -
|
||||
alpha * (old.internal_force[row] - old.external_force[row]);
|
||||
result->solved = (1 + alpha) * next.residual[row] - alpha * old.residual[row];
|
||||
double mass_action_scale = 0;
|
||||
double relation_defect_action = 0;
|
||||
for (std::size_t entry = mass.RowOffsets()[row];
|
||||
entry < mass.RowOffsets()[row + 1]; ++entry) {
|
||||
const auto column = mass.ColumnIndices()[entry];
|
||||
const double coefficient = mass.Values()[entry];
|
||||
const double a = next.acceleration[column];
|
||||
const double A = next.algorithmic_acceleration[column];
|
||||
const double a_old = old.acceleration[column];
|
||||
const double relation_defect = A - (1 + alpha) * a + alpha * a_old;
|
||||
const double action_scale = std::abs(coefficient * A) +
|
||||
(1 + alpha) * std::abs(coefficient * a) +
|
||||
std::abs(alpha) * std::abs(coefficient * a_old);
|
||||
const double defect_action = std::abs(coefficient * relation_defect);
|
||||
for (double x : {a, A, a_old, relation_defect, action_scale, defect_action})
|
||||
if (!std::isfinite(x)) return false;
|
||||
mass_action_scale += action_scale;
|
||||
relation_defect_action += defect_action;
|
||||
}
|
||||
result->scale = mass_action_scale + relation_defect_action +
|
||||
(1 + alpha) * (std::abs(next.internal_force[row]) +
|
||||
std::abs(next.external_force[row])) +
|
||||
std::abs(alpha) * (std::abs(old.internal_force[row]) +
|
||||
std::abs(old.external_force[row])) +
|
||||
(1 + alpha) * std::abs(next.residual[row]) +
|
||||
std::abs(alpha * old.residual[row]);
|
||||
result->defect = std::abs(result->original - result->solved);
|
||||
for (double x : {result->original, result->solved, mass_action_scale,
|
||||
relation_defect_action, result->scale, result->defect})
|
||||
if (!std::isfinite(x)) return false;
|
||||
result->metric =
|
||||
result->scale == 0
|
||||
? (result->defect == 0 ? 0 : std::numeric_limits<double>::infinity())
|
||||
: result->defect / result->scale;
|
||||
return std::isfinite(result->metric);
|
||||
}
|
||||
/// @brief Finite checked failure-shadow accumulation, separate from physics.
|
||||
inline bool AccumulateSolidHhtForceShadow(double term, double product_error,
|
||||
double* sum, double* correction,
|
||||
double* absolute_sum) {
|
||||
if (!std::isfinite(term) || !std::isfinite(product_error) ||
|
||||
!std::isfinite(*sum) || !std::isfinite(*correction) ||
|
||||
!std::isfinite(*absolute_sum))
|
||||
return false;
|
||||
const double next = *sum + term;
|
||||
const double absolute_next = *absolute_sum + std::abs(term);
|
||||
const double error_next = *correction + product_error;
|
||||
const double lost = std::abs(*sum) >= std::abs(term) ? (*sum - next) + term
|
||||
: (term - next) + *sum;
|
||||
const double correction_next = error_next + lost;
|
||||
if (!std::isfinite(next) || !std::isfinite(absolute_next) ||
|
||||
!std::isfinite(error_next) || !std::isfinite(lost) ||
|
||||
!std::isfinite(correction_next))
|
||||
return false;
|
||||
*sum = next;
|
||||
*correction = correction_next;
|
||||
*absolute_sum = absolute_next;
|
||||
return true;
|
||||
}
|
||||
} // namespace fesa::detail
|
||||
#endif // FESA_ANALYSIS_SOLID_HHT_FORCE_IDENTITY_H_
|
||||
@@ -0,0 +1,190 @@
|
||||
#ifndef FESA_ANALYSIS_SOLID_HHT_HIGH_LOW_H_
|
||||
#define FESA_ANALYSIS_SOLID_HHT_HIGH_LOW_H_
|
||||
|
||||
#include "fesa/analysis/hht_time_integration.h"
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
#include "math/high_low_arithmetic.h"
|
||||
|
||||
namespace fesa::detail {
|
||||
/// @brief Keeps both physical parts of the fixed-increment predictor.
|
||||
struct SolidHhtPairedPredictor {
|
||||
Vector displacement{0};
|
||||
Vector displacement_low{0};
|
||||
Vector velocity{0};
|
||||
Vector velocity_low{0};
|
||||
};
|
||||
|
||||
/// @brief Owns physical and auxiliary rates without collapsing either part.
|
||||
struct SolidHhtPairedRates {
|
||||
Vector velocity{0};
|
||||
Vector velocity_low{0};
|
||||
Vector acceleration{0};
|
||||
Vector acceleration_low{0};
|
||||
Vector algorithmic_acceleration{0};
|
||||
Vector algorithmic_acceleration_low{0};
|
||||
};
|
||||
|
||||
struct SolidHhtPairedVector {
|
||||
Vector high{0};
|
||||
Vector low{0};
|
||||
};
|
||||
|
||||
inline Status SolidHhtPairedKinematicsFailure() {
|
||||
return Status::Failure(FailureCategory::kSolver,
|
||||
{{Severity::kError,
|
||||
"solid-dynamic-invalid-paired-kinematics",
|
||||
{},
|
||||
"DYNAMIC",
|
||||
{},
|
||||
"Incomplete, unnormalized or nonfinite paired HHT "
|
||||
"kinematics."}});
|
||||
}
|
||||
|
||||
inline bool ValidSolidHhtPair(const Vector& high, const Vector& low,
|
||||
std::size_t size) {
|
||||
if (high.Size() != size || low.Size() != size) return false;
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
const math_internal::HighLow value(high[i], low[i]);
|
||||
if (!math_internal::IsFinite(value) || !math_internal::IsNormalized(value))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ValidSolidHhtParameters(double h, const HhtParameters& p) {
|
||||
if (!math_internal::ArithmeticEnvironmentSupported()) return false;
|
||||
const auto expected = MakeHhtParameters(p.alpha);
|
||||
if (!expected.HasValue() || p.beta != expected.Value().beta ||
|
||||
p.gamma != expected.Value().gamma)
|
||||
return false;
|
||||
const math_internal::HighLow step(h);
|
||||
const auto square = math_internal::Multiply(step, step);
|
||||
return math_internal::IsFinite(step) &&
|
||||
math_internal::Compare(step, math_internal::HighLow(0)) > 0 &&
|
||||
math_internal::IsFinite(square) &&
|
||||
math_internal::Compare(square, math_internal::HighLow(0)) > 0;
|
||||
}
|
||||
|
||||
/// @brief Uses the authoritative constant Float64 mass coefficients without
|
||||
/// collapsing either physical vector part or the ordered row accumulation.
|
||||
inline Result<SolidHhtPairedVector> MultiplySolidHhtMassHighLow(
|
||||
const SparseMatrix& mass, const Vector& high, const Vector& low) {
|
||||
using namespace math_internal;
|
||||
if (!ArithmeticEnvironmentSupported() || mass.Rows() != mass.Columns() ||
|
||||
!ValidSolidHhtPair(high, low, mass.Columns()))
|
||||
return Result<SolidHhtPairedVector>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
SolidHhtPairedVector result{Vector(mass.Rows()), Vector(mass.Rows())};
|
||||
for (std::size_t row = 0; row < mass.Rows(); ++row) {
|
||||
HighLow sum;
|
||||
for (std::size_t k = mass.RowOffsets()[row]; k < mass.RowOffsets()[row + 1];
|
||||
++k) {
|
||||
const auto col = mass.ColumnIndices()[k];
|
||||
sum = Add(sum, Multiply(HighLow(mass.Values()[k]),
|
||||
HighLow(high[col], low[col])));
|
||||
if (!IsFinite(sum) || !IsNormalized(sum))
|
||||
return Result<SolidHhtPairedVector>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
}
|
||||
result.high[row] = sum.high;
|
||||
result.low[row] = sum.low;
|
||||
}
|
||||
return Result<SolidHhtPairedVector>::Success(std::move(result));
|
||||
}
|
||||
|
||||
/// @brief Applies the unchanged Newmark predictor to the full physical state.
|
||||
inline Result<SolidHhtPairedPredictor> PredictSolidHhtHighLow(
|
||||
const SolidDynamicAcceptedFrameState& old, double h,
|
||||
const HhtParameters& p) {
|
||||
using namespace math_internal;
|
||||
const auto size = old.displacement.Size();
|
||||
if (!ValidSolidHhtPair(old.displacement, old.low.displacement, size) ||
|
||||
!ValidSolidHhtPair(old.velocity, old.low.velocity, size) ||
|
||||
!ValidSolidHhtPair(old.algorithmic_acceleration,
|
||||
old.low.algorithmic_acceleration, size) ||
|
||||
!ValidSolidHhtParameters(h, p))
|
||||
return Result<SolidHhtPairedPredictor>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
SolidHhtPairedPredictor result{Vector(size), Vector(size), Vector(size),
|
||||
Vector(size)};
|
||||
const auto hh = Multiply(HighLow(h), HighLow(h));
|
||||
const auto u_coefficient =
|
||||
Multiply(hh, Subtract(HighLow(.5), HighLow(p.beta)));
|
||||
const auto v_coefficient =
|
||||
Multiply(HighLow(h), Subtract(HighLow(1), HighLow(p.gamma)));
|
||||
if (!IsFinite(u_coefficient) || !IsFinite(v_coefficient))
|
||||
return Result<SolidHhtPairedPredictor>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
const HighLow u(old.displacement[i], old.low.displacement[i]);
|
||||
const HighLow v(old.velocity[i], old.low.velocity[i]);
|
||||
const HighLow A(old.algorithmic_acceleration[i],
|
||||
old.low.algorithmic_acceleration[i]);
|
||||
const auto up =
|
||||
Add(Add(u, Multiply(HighLow(h), v)), Multiply(u_coefficient, A));
|
||||
const auto vp = Add(v, Multiply(v_coefficient, A));
|
||||
if (!IsFinite(up) || !IsNormalized(up) || !IsFinite(vp) ||
|
||||
!IsNormalized(vp))
|
||||
return Result<SolidHhtPairedPredictor>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
result.displacement[i] = up.high;
|
||||
result.displacement_low[i] = up.low;
|
||||
result.velocity[i] = vp.high;
|
||||
result.velocity_low[i] = vp.low;
|
||||
}
|
||||
return Result<SolidHhtPairedPredictor>::Success(std::move(result));
|
||||
}
|
||||
|
||||
/// @brief Recovers A, v and physical a from the paired displacement difference.
|
||||
inline Result<SolidHhtPairedRates> EvaluateSolidHhtHighLowRates(
|
||||
const Vector& high, const Vector& low,
|
||||
const SolidHhtPairedPredictor& predictor, const Vector& old_acceleration,
|
||||
const Vector& old_acceleration_low, double h, const HhtParameters& p) {
|
||||
using namespace math_internal;
|
||||
const auto size = high.Size();
|
||||
if (!ValidSolidHhtPair(high, low, size) ||
|
||||
!ValidSolidHhtPair(predictor.displacement, predictor.displacement_low,
|
||||
size) ||
|
||||
!ValidSolidHhtPair(predictor.velocity, predictor.velocity_low, size) ||
|
||||
!ValidSolidHhtPair(old_acceleration, old_acceleration_low, size) ||
|
||||
!ValidSolidHhtParameters(h, p))
|
||||
return Result<SolidHhtPairedRates>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
SolidHhtPairedRates result{Vector(size), Vector(size), Vector(size),
|
||||
Vector(size), Vector(size), Vector(size)};
|
||||
const auto denominator =
|
||||
Multiply(Multiply(HighLow(p.beta), HighLow(h)), HighLow(h));
|
||||
const auto velocity_coefficient = Multiply(HighLow(p.gamma), HighLow(h));
|
||||
const auto physical_coefficient = Add(HighLow(1), HighLow(p.alpha));
|
||||
if (!IsFinite(denominator) || Compare(denominator, HighLow(0)) <= 0 ||
|
||||
!IsFinite(velocity_coefficient) || !IsFinite(physical_coefficient))
|
||||
return Result<SolidHhtPairedRates>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
for (std::size_t i = 0; i < size; ++i) {
|
||||
const auto A = Divide(Subtract(HighLow(high[i], low[i]),
|
||||
HighLow(predictor.displacement[i],
|
||||
predictor.displacement_low[i])),
|
||||
denominator);
|
||||
const auto v =
|
||||
Add(HighLow(predictor.velocity[i], predictor.velocity_low[i]),
|
||||
Multiply(velocity_coefficient, A));
|
||||
const auto a = Divide(
|
||||
Add(A, Multiply(HighLow(p.alpha),
|
||||
HighLow(old_acceleration[i], old_acceleration_low[i]))),
|
||||
physical_coefficient);
|
||||
if (!IsFinite(A) || !IsNormalized(A) || !IsFinite(v) || !IsNormalized(v) ||
|
||||
!IsFinite(a) || !IsNormalized(a))
|
||||
return Result<SolidHhtPairedRates>::Failure(
|
||||
SolidHhtPairedKinematicsFailure());
|
||||
result.algorithmic_acceleration[i] = A.high;
|
||||
result.algorithmic_acceleration_low[i] = A.low;
|
||||
result.velocity[i] = v.high;
|
||||
result.velocity_low[i] = v.low;
|
||||
result.acceleration[i] = a.high;
|
||||
result.acceleration_low[i] = a.low;
|
||||
}
|
||||
return Result<SolidHhtPairedRates>::Success(std::move(result));
|
||||
}
|
||||
} // namespace fesa::detail
|
||||
#endif // FESA_ANALYSIS_SOLID_HHT_HIGH_LOW_H_
|
||||
@@ -0,0 +1,217 @@
|
||||
#ifndef FESA_ANALYSIS_SOLID_HHT_REPRESENTABILITY_H_
|
||||
#define FESA_ANALYSIS_SOLID_HHT_REPRESENTABILITY_H_
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/solid_dynamic_analysis_state.h"
|
||||
#include "fesa/math/sparse_matrix.h"
|
||||
namespace fesa::detail {
|
||||
struct SolidHhtRepresentableNeighbor {
|
||||
bool found{false};
|
||||
std::size_t full_dof{0};
|
||||
double displacement{0};
|
||||
double delta{0};
|
||||
double predicted_metric{0};
|
||||
};
|
||||
/// @brief Predicts a strictly improving one-ULP neighbor using separate
|
||||
/// physical inertia/internal-force actions and every original free score.
|
||||
/// This selects a trial only; callers must recompute actual endpoint gates.
|
||||
inline bool SelectSolidHhtRepresentableNeighbor(
|
||||
const SparseMatrix& mass, const SparseMatrix& tangent,
|
||||
const std::vector<std::size_t>& free_dofs,
|
||||
const SolidDynamicAcceptedFrameState& frame, double h, double alpha,
|
||||
double beta, SolidHhtRepresentableNeighbor* selected,
|
||||
std::vector<SolidHhtRepresentableNeighbor>* ranked = nullptr) {
|
||||
*selected = {};
|
||||
if (ranked != nullptr) ranked->clear();
|
||||
const auto size = mass.Rows();
|
||||
if (tangent.Rows() != size || mass.Columns() != size ||
|
||||
tangent.Columns() != size || mass.RowOffsets() != tangent.RowOffsets() ||
|
||||
mass.ColumnIndices() != tangent.ColumnIndices() ||
|
||||
frame.displacement.Size() != size || frame.inertia_force.Size() != size ||
|
||||
frame.internal_force.Size() != size ||
|
||||
frame.external_force.Size() != size || frame.residual.Size() != size ||
|
||||
frame.residual_scale.Size() != size)
|
||||
return false;
|
||||
const double inertia_factor = 1 / ((1 + alpha) * beta * h * h);
|
||||
if (!std::isfinite(inertia_factor) || inertia_factor <= 0) return false;
|
||||
const auto score = [](double ma, double f, double F, double* metric) {
|
||||
const double scale = std::abs(ma) + std::abs(f) + std::abs(F);
|
||||
const double r = ma + f - F;
|
||||
if (!std::isfinite(scale) || !std::isfinite(r)) return false;
|
||||
*metric = scale == 0
|
||||
? (r == 0 ? 0 : std::numeric_limits<double>::infinity())
|
||||
: std::abs(r) / scale;
|
||||
return std::isfinite(*metric);
|
||||
};
|
||||
std::vector<bool> is_free(size, false);
|
||||
std::vector<double> scores(size, 0);
|
||||
double best = 0;
|
||||
for (auto i : free_dofs) {
|
||||
if (i >= size || !std::isfinite(frame.displacement[i]) ||
|
||||
!score(frame.inertia_force[i], frame.internal_force[i],
|
||||
frame.external_force[i], &scores[i]))
|
||||
return false;
|
||||
is_free[i] = true;
|
||||
best = (std::max)(best, scores[i]);
|
||||
}
|
||||
struct Action {
|
||||
std::size_t row;
|
||||
double inertia;
|
||||
double internal;
|
||||
};
|
||||
std::vector<std::vector<Action>> columns(size);
|
||||
for (auto i : free_dofs)
|
||||
for (std::size_t k = mass.RowOffsets()[i]; k < mass.RowOffsets()[i + 1];
|
||||
++k) {
|
||||
const auto j = mass.ColumnIndices()[k];
|
||||
if (!is_free[j]) continue;
|
||||
const double inertia = mass.Values()[k] * inertia_factor;
|
||||
const double internal = tangent.Values()[k];
|
||||
if (!std::isfinite(inertia) || !std::isfinite(internal)) return false;
|
||||
columns[j].push_back({i, inertia, internal});
|
||||
}
|
||||
const double original_metric = best;
|
||||
auto predicted = scores;
|
||||
for (auto j : free_dofs)
|
||||
for (const double direction : {-std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::infinity()}) {
|
||||
const double proposed = std::nextafter(frame.displacement[j], direction);
|
||||
const double delta = proposed - frame.displacement[j];
|
||||
if (!std::isfinite(proposed) || !std::isfinite(delta) || delta == 0)
|
||||
continue;
|
||||
predicted = scores;
|
||||
for (const auto& action : columns[j]) {
|
||||
const auto i = action.row;
|
||||
const double ma = frame.inertia_force[i] + action.inertia * delta;
|
||||
const double f = frame.internal_force[i] + action.internal * delta;
|
||||
if (!score(ma, f, frame.external_force[i], &predicted[i])) return false;
|
||||
}
|
||||
double metric = 0;
|
||||
for (auto i : free_dofs) metric = (std::max)(metric, predicted[i]);
|
||||
if (ranked != nullptr && metric < original_metric)
|
||||
ranked->push_back({true, j, proposed, delta, metric});
|
||||
// Stable full-DOF/direction ordering resolves ties without a floor.
|
||||
if (metric < best) {
|
||||
best = metric;
|
||||
*selected = {true, j, proposed, delta, metric};
|
||||
}
|
||||
}
|
||||
if (ranked != nullptr)
|
||||
std::stable_sort(ranked->begin(), ranked->end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return a.predicted_metric < b.predicted_metric;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/// @brief Keeps complete eligible failed Newton entries for a fixed increment.
|
||||
class SolidHhtNewtonHistory {
|
||||
public:
|
||||
bool Capture(const Vector& u, const std::vector<bool>& holding_mask,
|
||||
const std::array<double, 4>& incoming_metrics,
|
||||
std::uint32_t iteration, bool physical_pass) {
|
||||
if (!Valid(u, holding_mask, incoming_metrics)) return false;
|
||||
if (!physical_pass &&
|
||||
std::all_of(incoming_metrics.begin(), incoming_metrics.end(),
|
||||
[](double metric) { return metric <= 1e-10; }))
|
||||
entries_.push_back({u, holding_mask, incoming_metrics, iteration});
|
||||
return true;
|
||||
}
|
||||
bool Match(const Vector& proposed_u,
|
||||
const std::vector<bool>& next_holding_mask,
|
||||
const std::array<double, 4>& fresh_metrics, bool physical_pass,
|
||||
SolidDynamicNewtonRepeatEvidence* evidence) const {
|
||||
*evidence = {};
|
||||
evidence->history_entry_count = entries_.size();
|
||||
if (!Valid(proposed_u, next_holding_mask, fresh_metrics)) return false;
|
||||
// A physically converged prospective endpoint must reach the normal gate.
|
||||
if (physical_pass ||
|
||||
!std::all_of(fresh_metrics.begin(), fresh_metrics.end(),
|
||||
[](double metric) { return metric <= 1e-10; }))
|
||||
return true;
|
||||
for (const auto& entry : entries_) {
|
||||
evidence->changed_translations = 0;
|
||||
evidence->changed_holding_mask = 0;
|
||||
evidence->changed_correction_metrics = 0;
|
||||
for (std::size_t i = 0; i < proposed_u.Size(); ++i)
|
||||
evidence->changed_translations += proposed_u[i] != entry.u[i];
|
||||
for (std::size_t i = 0; i < next_holding_mask.size(); ++i)
|
||||
evidence->changed_holding_mask +=
|
||||
next_holding_mask[i] != entry.holding_mask[i];
|
||||
for (std::size_t i = 0; i < 4; ++i)
|
||||
evidence->changed_correction_metrics +=
|
||||
fresh_metrics[i] != entry.incoming_metrics[i];
|
||||
// Numeric correction magnitudes remain evidence. Their finite/PASS
|
||||
// controls, rather than their exact magnitudes, affect the transition.
|
||||
if (evidence->changed_translations == 0 &&
|
||||
evidence->changed_holding_mask == 0) {
|
||||
evidence->matched_entry_iteration = entry.iteration;
|
||||
evidence->repeat = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::size_t Size() const { return entries_.size(); }
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
Vector u;
|
||||
std::vector<bool> holding_mask;
|
||||
std::array<double, 4> incoming_metrics;
|
||||
std::uint32_t iteration;
|
||||
};
|
||||
bool Valid(const Vector& u, const std::vector<bool>& mask,
|
||||
const std::array<double, 4>& metrics) const {
|
||||
if (mask.size() > u.Size() ||
|
||||
(!entries_.empty() &&
|
||||
(u.Size() != entries_.front().u.Size() ||
|
||||
mask.size() != entries_.front().holding_mask.size())))
|
||||
return false;
|
||||
for (std::size_t i = 0; i < u.Size(); ++i)
|
||||
if (!std::isfinite(u[i])) return false;
|
||||
for (double metric : metrics)
|
||||
if (!std::isfinite(metric) || metric < 0) return false;
|
||||
return true;
|
||||
}
|
||||
std::vector<Entry> entries_;
|
||||
};
|
||||
/// @brief Checks the fresh complete Newton trial before representable fallback.
|
||||
/// Historical scores are deliberately absent: a lower half of a two-cycle
|
||||
/// still needs actual proposed-step evaluation. False keeps common Newton
|
||||
/// apply.
|
||||
template <class Newton, class Neighbor>
|
||||
Result<bool> TrySolidHhtActualStep(
|
||||
bool residual_pass, const std::array<double, 4>& fresh_corrections,
|
||||
Newton newton, Neighbor neighbor) {
|
||||
if (residual_pass || !std::all_of(fresh_corrections.begin(),
|
||||
fresh_corrections.end(), [](double metric) {
|
||||
return std::isfinite(metric) &&
|
||||
metric <= 1e-10;
|
||||
}))
|
||||
return Result<bool>::Success(false);
|
||||
auto keep_newton = newton();
|
||||
if (!keep_newton.HasValue())
|
||||
return Result<bool>::Failure(keep_newton.GetStatus());
|
||||
if (keep_newton.Value()) return Result<bool>::Success(false);
|
||||
return neighbor();
|
||||
}
|
||||
/// @brief Tries ranked predictions until actual endpoint evaluation improves.
|
||||
/// Evaluator failure is terminal; a rejected prediction does not discard later
|
||||
/// candidates, and success means only that a local trial improved.
|
||||
template <class Evaluate>
|
||||
Result<bool> TrySolidHhtRankedNeighbors(
|
||||
const std::vector<SolidHhtRepresentableNeighbor>& ranked,
|
||||
Evaluate evaluate) {
|
||||
for (const auto& neighbor : ranked) {
|
||||
auto actual = evaluate(neighbor);
|
||||
if (!actual.HasValue()) return Result<bool>::Failure(actual.GetStatus());
|
||||
if (actual.Value()) return Result<bool>::Success(true);
|
||||
}
|
||||
return Result<bool>::Success(false);
|
||||
}
|
||||
} // namespace fesa::detail
|
||||
#endif // FESA_ANALYSIS_SOLID_HHT_REPRESENTABILITY_H_
|
||||
@@ -19,6 +19,8 @@ add_executable(
|
||||
unit/analysis/nonlinear_static_control_test.cpp
|
||||
unit/analysis/newton_iteration_log_test.cpp
|
||||
unit/analysis/solid_frequency_procedure_test.cpp
|
||||
unit/analysis/solid_hht_dynamic_procedure_test.cpp
|
||||
unit/analysis/solid_hht_temporal_verification_test.cpp
|
||||
unit/analysis/solid_static_procedure_test.cpp
|
||||
unit/analysis/solid_static_state_test.cpp
|
||||
unit/analysis/thermal_time_grid_test.cpp
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,653 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/analysis/solid_hht_dynamic_procedure.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/elements/hex8_element.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
#include "solid_hht_test_fixture.h"
|
||||
|
||||
namespace {
|
||||
using M3 = std::array<std::array<double, 3>, 3>;
|
||||
struct AffineMotion {
|
||||
M3 b{}, velocity{}, acceleration{}, piola{};
|
||||
};
|
||||
AffineMotion Motion(double time, bool nonlinear) {
|
||||
const double q = 1 - std::cos(2 * time);
|
||||
const double dq = 2 * std::sin(2 * time);
|
||||
const double ddq = 4 * std::cos(2 * time);
|
||||
AffineMotion m;
|
||||
constexpr double lambda = 4.8;
|
||||
constexpr double mu = 4.8;
|
||||
if (!nonlinear) {
|
||||
const M3 d{{{.015, .008, -.004}, {.006, -.01, .003}, {.005, .004, .012}}};
|
||||
double trace = 0;
|
||||
for (unsigned i = 0; i < 3; ++i) trace += q * d[i][i];
|
||||
for (unsigned i = 0; i < 3; ++i)
|
||||
for (unsigned j = 0; j < 3; ++j) {
|
||||
m.b[i][j] = q * d[i][j];
|
||||
m.velocity[i][j] = dq * d[i][j];
|
||||
m.acceleration[i][j] = ddq * d[i][j];
|
||||
m.piola[i][j] =
|
||||
(i == j ? lambda * trace : 0) + mu * q * (d[i][j] + d[j][i]);
|
||||
}
|
||||
} else {
|
||||
const double s = .004 * q, ds = .004 * dq, dds = .004 * ddq;
|
||||
const double theta = .5 * q, dt = .5 * dq, ddt = .5 * ddq;
|
||||
const double c = std::cos(theta), sn = std::sin(theta);
|
||||
const M3 r{{{c, -sn, 0}, {sn, c, 0}, {0, 0, 1}}};
|
||||
const M3 jr{{{-sn, -c, 0}, {c, -sn, 0}, {0, 0, 0}}};
|
||||
const M3 jjr{{{-c, sn, 0}, {-sn, -c, 0}, {0, 0, 0}}};
|
||||
const double eg = .5 * ((1 + s) * (1 + s) - 1);
|
||||
const double stress = (3 * lambda + 2 * mu) * eg;
|
||||
for (unsigned i = 0; i < 3; ++i)
|
||||
for (unsigned j = 0; j < 3; ++j) {
|
||||
m.b[i][j] = (1 + s) * r[i][j] - (i == j ? 1 : 0);
|
||||
m.velocity[i][j] = ds * r[i][j] + (1 + s) * dt * jr[i][j];
|
||||
m.acceleration[i][j] = dds * r[i][j] + 2 * ds * dt * jr[i][j] +
|
||||
(1 + s) * (ddt * jr[i][j] + dt * dt * jjr[i][j]);
|
||||
m.piola[i][j] = (1 + s) * r[i][j] * stress;
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
std::array<double, 3> Apply(const M3& matrix, const std::array<double, 3>& x) {
|
||||
std::array<double, 3> result{};
|
||||
for (unsigned i = 0; i < 3; ++i)
|
||||
for (unsigned j = 0; j < 3; ++j) result[i] += matrix[i][j] * x[j];
|
||||
return result;
|
||||
}
|
||||
fesa::ModelDefinition TwoBricks(double h, unsigned count, bool nonlinear) {
|
||||
auto d = solid_hht_test::Brick(h, count, nonlinear);
|
||||
const std::array<std::array<double, 3>, 4> added{
|
||||
{{2, 0, 0}, {2, 1, 0}, {2, 0, 1}, {2, 1, 1}}};
|
||||
for (unsigned j = 0; j < 4; ++j) {
|
||||
unsigned n = 8 + j;
|
||||
d.nodes.push_back({{"I", n + 1, std::to_string(n + 1)}, added[j], {}});
|
||||
d.node_sets.push_back({"N" + std::to_string(n), {}, {n}, {}});
|
||||
}
|
||||
d.solid_elements.emplace_back(
|
||||
fesa::SourceEntityId{"I", 9, "9"},
|
||||
std::array<fesa::EntityIndex, 8>{1, 8, 9, 2, 5, 10, 11, 6}, 0, 0,
|
||||
fesa::SourceLocation{});
|
||||
return d;
|
||||
}
|
||||
// Closed polynomial brick integrals: integral Na*X and integral grad Na.
|
||||
// These do not call the production stiffness, mass, quadrature or response.
|
||||
std::vector<double> ExactForce(const fesa::ModelDefinition& d, double t,
|
||||
bool nonlinear) {
|
||||
const auto m = Motion(t, nonlinear);
|
||||
std::vector<double> f(3 * d.nodes.size());
|
||||
for (unsigned e = 0; e < 2; ++e) {
|
||||
const auto& nodes = d.solid_elements[e].node_indices;
|
||||
for (unsigned a = 0; a < 8; ++a) {
|
||||
const auto& x = d.nodes[nodes[a]].coordinates;
|
||||
std::array<double, 3> integral_x{}, gradient{};
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const double origin = c == 0 ? static_cast<double>(e) : 0;
|
||||
const double local = x[c] - origin;
|
||||
integral_x[c] = 2 * (origin / 8 + (1 + local) / 24);
|
||||
gradient[c] = (2 * local - 1) / 4;
|
||||
}
|
||||
const auto inertia = Apply(m.acceleration, integral_x);
|
||||
const auto internal = Apply(m.piola, gradient);
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
f[3 * nodes[a] + c] += inertia[c] + internal[c];
|
||||
}
|
||||
}
|
||||
return f;
|
||||
}
|
||||
std::vector<double> ExactMassProduct(const fesa::Domain& d,
|
||||
const fesa::Vector& v) {
|
||||
std::vector<double> result(v.Size());
|
||||
for (unsigned e = 0; e < 2; ++e) {
|
||||
const auto& nodes = d.SolidElements()[e].node_indices;
|
||||
for (unsigned a = 0; a < 8; ++a)
|
||||
for (unsigned b = 0; b < 8; ++b) {
|
||||
double coefficient = 2.0 / 216;
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
coefficient *= d.Nodes()[nodes[a]].coordinates[c] ==
|
||||
d.Nodes()[nodes[b]].coordinates[c]
|
||||
? 2
|
||||
: 1;
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
result[3 * nodes[a] + c] += coefficient * v[3 * nodes[b] + c];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void AddCross(const std::array<double, 3>& x, const std::array<double, 3>& f,
|
||||
std::array<double, 3>* result) {
|
||||
(*result)[0] += x[1] * f[2] - x[2] * f[1];
|
||||
(*result)[1] += x[2] * f[0] - x[0] * f[2];
|
||||
(*result)[2] += x[0] * f[1] - x[1] * f[0];
|
||||
}
|
||||
std::array<double, 3> ManufacturedRun(double h, unsigned count,
|
||||
bool nonlinear,
|
||||
bool compare_parallel = false) {
|
||||
auto d = TwoBricks(h, count, nonlinear);
|
||||
for (unsigned i = 0; i < 3 * d.nodes.size(); ++i) {
|
||||
fesa::TabularAmplitudeDefinition amplitude;
|
||||
amplitude.name_raw = "A" + std::to_string(i);
|
||||
amplitude.name_canonical = amplitude.name_raw;
|
||||
for (unsigned n = 0; n <= count; ++n) {
|
||||
const double t = static_cast<double>(n) * h;
|
||||
amplitude.points.push_back({t, ExactForce(d, t, nonlinear)[i]});
|
||||
}
|
||||
d.amplitudes.push_back(amplitude);
|
||||
d.dynamic_step->loads.push_back({"N" + std::to_string(i / 3),
|
||||
static_cast<int>(i % 3 + 1),
|
||||
1,
|
||||
{},
|
||||
i,
|
||||
amplitude.name_raw,
|
||||
{}});
|
||||
}
|
||||
auto domain = fesa::Domain::Create(std::move(d));
|
||||
EXPECT_TRUE(domain.HasValue());
|
||||
if (!domain.HasValue()) return {1, 1, 1};
|
||||
fesa::SerialParallelFor parallel;
|
||||
fesa::MklPardisoSolver mass;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
auto model = fesa::AnalysisModel::Create(domain.Value());
|
||||
auto dofs = fesa::DofManager::Create(model.Value());
|
||||
auto state =
|
||||
fesa::SolidDynamicAnalysisState::Create(domain.Value(), dofs.Value());
|
||||
auto status = fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.ExecuteInto(domain.Value(), state.Value());
|
||||
EXPECT_TRUE(status.IsOk()) << solid_hht_test::Message(status);
|
||||
if (!status.IsOk()) {
|
||||
const auto& e = state.Value().IterationRecords().back().evidence;
|
||||
std::cout << "Failed force metric " << e.residual_metric << " update "
|
||||
<< e.update_metric << '\n';
|
||||
return {1, 1, 1};
|
||||
}
|
||||
if (count == 30 || compare_parallel) {
|
||||
// Both element workers have distinct index-owned buffers; compare every
|
||||
// accepted field, not merely the final displacement norm.
|
||||
auto parallel_state =
|
||||
fesa::SolidDynamicAnalysisState::Create(domain.Value(), dofs.Value());
|
||||
fesa::TbbParallelFor tbb;
|
||||
fesa::MklPardisoSolver parallel_mass;
|
||||
fesa::MklPardisoTangentSolver parallel_tangent;
|
||||
auto parallel_status =
|
||||
fesa::SolidHhtDynamicProcedureExecutor(tbb, parallel_mass,
|
||||
parallel_tangent)
|
||||
.ExecuteInto(domain.Value(), parallel_state.Value());
|
||||
EXPECT_TRUE(parallel_status.IsOk())
|
||||
<< solid_hht_test::Message(parallel_status);
|
||||
if (parallel_status.IsOk()) {
|
||||
const auto& serial_frames = state.Value().AcceptedFrames();
|
||||
const auto& parallel_frames = parallel_state.Value().AcceptedFrames();
|
||||
EXPECT_EQ(serial_frames.size(), parallel_frames.size());
|
||||
bool has_nonzero_low = false;
|
||||
for (std::size_t n = 0; n < serial_frames.size(); ++n) {
|
||||
const auto& sf = serial_frames[n];
|
||||
const auto& pf = parallel_frames[n];
|
||||
EXPECT_DOUBLE_EQ(sf.kinetic_energy, pf.kinetic_energy);
|
||||
EXPECT_DOUBLE_EQ(sf.strain_energy, pf.strain_energy);
|
||||
EXPECT_DOUBLE_EQ(sf.external_work, pf.external_work);
|
||||
EXPECT_EQ(sf.angular_momentum, pf.angular_momentum);
|
||||
EXPECT_EQ(sf.low.strain_energy, pf.low.strain_energy);
|
||||
EXPECT_EQ(sf.low.kinetic_energy, pf.low.kinetic_energy);
|
||||
EXPECT_EQ(sf.low.external_work, pf.low.external_work);
|
||||
EXPECT_EQ(sf.low.energy_balance_error, pf.low.energy_balance_error);
|
||||
EXPECT_EQ(sf.low.linear_momentum, pf.low.linear_momentum);
|
||||
EXPECT_EQ(sf.low.angular_momentum, pf.low.angular_momentum);
|
||||
EXPECT_EQ(sf.low.linear_momentum_rate, pf.low.linear_momentum_rate);
|
||||
EXPECT_EQ(sf.low.angular_momentum_rate, pf.low.angular_momentum_rate);
|
||||
EXPECT_EQ(sf.low.global_equilibrium, pf.low.global_equilibrium);
|
||||
EXPECT_EQ(sf.low.linearization_current_internal_moment,
|
||||
pf.low.linearization_current_internal_moment);
|
||||
EXPECT_EQ(sf.low.reference_internal_moment,
|
||||
pf.low.reference_internal_moment);
|
||||
EXPECT_EQ(sf.low.current_free_residual_moment,
|
||||
pf.low.current_free_residual_moment);
|
||||
for (auto member : {&fesa::SolidDynamicFrameLowParts::displacement,
|
||||
&fesa::SolidDynamicFrameLowParts::velocity,
|
||||
&fesa::SolidDynamicFrameLowParts::acceleration,
|
||||
&fesa::SolidDynamicFrameLowParts::algorithmic_acceleration,
|
||||
&fesa::SolidDynamicFrameLowParts::inertia_force,
|
||||
&fesa::SolidDynamicFrameLowParts::internal_force,
|
||||
&fesa::SolidDynamicFrameLowParts::external_force,
|
||||
&fesa::SolidDynamicFrameLowParts::residual,
|
||||
&fesa::SolidDynamicFrameLowParts::reaction,
|
||||
&fesa::SolidDynamicFrameLowParts::residual_scale}) {
|
||||
const auto& sl = sf.low.*member;
|
||||
const auto& pl = pf.low.*member;
|
||||
EXPECT_EQ(sl.Size(), pl.Size());
|
||||
if (sl.Size() != pl.Size()) continue;
|
||||
for (std::size_t i = 0; i < sl.Size(); ++i) {
|
||||
EXPECT_EQ(sl[i], pl[i]);
|
||||
has_nonzero_low = has_nonzero_low || sl[i] != 0;
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < sf.displacement.Size(); ++i) {
|
||||
EXPECT_DOUBLE_EQ(sf.displacement[i], pf.displacement[i]);
|
||||
EXPECT_DOUBLE_EQ(sf.velocity[i], pf.velocity[i]);
|
||||
EXPECT_DOUBLE_EQ(sf.acceleration[i], pf.acceleration[i]);
|
||||
EXPECT_DOUBLE_EQ(sf.algorithmic_acceleration[i],
|
||||
pf.algorithmic_acceleration[i]);
|
||||
EXPECT_DOUBLE_EQ(sf.residual[i], pf.residual[i]);
|
||||
}
|
||||
for (std::size_t point = 0; point < sf.solid_results.size(); ++point) {
|
||||
EXPECT_EQ(sf.solid_results[point].cauchy_stress,
|
||||
pf.solid_results[point].cauchy_stress);
|
||||
EXPECT_EQ(sf.solid_results[point].strain,
|
||||
pf.solid_results[point].strain);
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(has_nonzero_low);
|
||||
}
|
||||
}
|
||||
std::array<double, 3> errors{}, scales{};
|
||||
const auto exact = Motion(static_cast<double>(count) * h, nonlinear);
|
||||
const auto& final = state.Value().CommittedState();
|
||||
for (unsigned node = 0; node < domain.Value().Nodes().size(); ++node) {
|
||||
const auto u = Apply(exact.b, domain.Value().Nodes()[node].coordinates);
|
||||
const auto v =
|
||||
Apply(exact.velocity, domain.Value().Nodes()[node].coordinates);
|
||||
const auto a =
|
||||
Apply(exact.acceleration, domain.Value().Nodes()[node].coordinates);
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const std::array<double, 3> truth{u[c], v[c], a[c]};
|
||||
const std::array<double, 3> actual{final.displacement[3 * node + c],
|
||||
final.velocity[3 * node + c],
|
||||
final.acceleration[3 * node + c]};
|
||||
for (unsigned j = 0; j < 3; ++j) {
|
||||
errors[j] += (actual[j] - truth[j]) * (actual[j] - truth[j]);
|
||||
scales[j] += truth[j] * truth[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (unsigned j = 0; j < 3; ++j) errors[j] = std::sqrt(errors[j] / scales[j]);
|
||||
for (const auto& f : state.Value().AcceptedFrames()) {
|
||||
const auto mv = ExactMassProduct(domain.Value(), f.velocity);
|
||||
const auto ma = ExactMassProduct(domain.Value(), f.acceleration);
|
||||
std::array<double, 3> p{}, angular{}, pdot{}, ldot{}, vv{};
|
||||
double kinetic = 0, energy_scale = 0, momentum_scale = 0;
|
||||
double rate_scale = 0, moment_scale = 0, angular_scale = 0, vv_scale = 0;
|
||||
double free_residual_sum = 0;
|
||||
for (unsigned n = 0; n < domain.Value().Nodes().size(); ++n) {
|
||||
auto x = domain.Value().Nodes()[n].coordinates;
|
||||
std::array<double, 3> nmv{}, nma{}, v{};
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
x[c] += f.displacement[3 * n + c];
|
||||
v[c] = f.velocity[3 * n + c];
|
||||
nmv[c] = mv[3 * n + c];
|
||||
nma[c] = ma[3 * n + c];
|
||||
p[c] += nmv[c];
|
||||
pdot[c] += nma[c];
|
||||
kinetic += .5 * v[c] * nmv[c];
|
||||
energy_scale += .5 * std::abs(v[c] * nmv[c]);
|
||||
momentum_scale += std::abs(nmv[c]);
|
||||
rate_scale += std::abs(nma[c]) + std::abs(f.internal_force[3 * n + c]) +
|
||||
std::abs(f.external_force[3 * n + c]) +
|
||||
std::abs(f.reaction[3 * n + c]);
|
||||
free_residual_sum +=
|
||||
std::abs(f.residual[3 * n + c] - f.reaction[3 * n + c]);
|
||||
}
|
||||
double arm = 0, velocity_scale = 0, node_mv = 0, node_rate = 0;
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
arm +=
|
||||
std::abs(x[c]) + std::abs(domain.Value().Nodes()[n].coordinates[c]);
|
||||
velocity_scale += std::abs(v[c]);
|
||||
node_mv += std::abs(nmv[c]);
|
||||
node_rate += std::abs(nma[c]) + std::abs(f.internal_force[3 * n + c]) +
|
||||
std::abs(f.external_force[3 * n + c]) +
|
||||
std::abs(f.reaction[3 * n + c]);
|
||||
}
|
||||
angular_scale += arm * node_mv;
|
||||
moment_scale += arm * node_rate;
|
||||
vv_scale += velocity_scale * node_mv;
|
||||
AddCross(x, nmv, &angular);
|
||||
AddCross(x, nma, &ldot);
|
||||
AddCross(v, nmv, &vv);
|
||||
}
|
||||
const double roundoff = 512 * std::numeric_limits<double>::epsilon();
|
||||
EXPECT_NEAR(f.kinetic_energy, kinetic, roundoff * energy_scale);
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
EXPECT_NEAR(f.linear_momentum[c], p[c], roundoff * momentum_scale);
|
||||
EXPECT_NEAR(f.angular_momentum[c], angular[c], roundoff * angular_scale);
|
||||
EXPECT_NEAR(f.linear_momentum_rate[c], pdot[c], roundoff * rate_scale);
|
||||
EXPECT_NEAR(f.angular_momentum_rate[c], ldot[c], roundoff * moment_scale);
|
||||
EXPECT_NEAR(vv[c], 0, roundoff * vv_scale);
|
||||
EXPECT_NEAR(f.global_equilibrium[c], 0,
|
||||
free_residual_sum + roundoff * rate_scale);
|
||||
const double identity = f.reference_internal_moment[c] +
|
||||
f.linearization_current_internal_moment[c] -
|
||||
f.current_free_residual_moment[c];
|
||||
EXPECT_NEAR(f.global_equilibrium[3 + c], identity,
|
||||
roundoff * moment_scale);
|
||||
if (nonlinear)
|
||||
EXPECT_NEAR(f.reference_internal_moment[c] +
|
||||
f.linearization_current_internal_moment[c],
|
||||
0, roundoff * moment_scale);
|
||||
else
|
||||
EXPECT_NEAR(f.reference_internal_moment[c], 0, roundoff * moment_scale);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(SolidHhtTemporal, PairedSerialAndTbbPreserveEveryLowComponent) {
|
||||
for (bool nonlinear : {false, true}) {
|
||||
SCOPED_TRACE(nonlinear);
|
||||
ManufacturedRun(.02, 2, nonlinear, true);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal, FreeConstantForceHasAnalyticalInitialAcceleration) {
|
||||
auto d = solid_hht_test::Brick();
|
||||
d.dynamic_step->boundaries = {{"ALL", 2, 3, 0, {}}};
|
||||
d.dynamic_step->loads = {{"ALL", 1, 0.25, {}}};
|
||||
auto domain = fesa::Domain::Create(std::move(d));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
fesa::SerialParallelFor parallel;
|
||||
fesa::MklPardisoSolver mass;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
auto result = fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.Execute(domain.Value());
|
||||
ASSERT_TRUE(result.HasValue()) << solid_hht_test::Message(result.GetStatus());
|
||||
const auto& initial = result.Value().state.AcceptedFrames().front();
|
||||
// Brick mass row sum rho V/8 = 1/4; rigid translation has zero stiffness.
|
||||
for (unsigned node = 0; node < 8; ++node) {
|
||||
EXPECT_NEAR(initial.acceleration[3 * node], 1, 1e-13);
|
||||
EXPECT_DOUBLE_EQ(initial.displacement[3 * node], 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal,
|
||||
AllFreeUniformForceRetainsExactRigidTranslationBehavior) {
|
||||
auto d = solid_hht_test::Brick();
|
||||
d.dynamic_step->loads = {{"ALL", 1, .25, {}}};
|
||||
auto domain = fesa::Domain::Create(std::move(d));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
fesa::SerialParallelFor parallel;
|
||||
fesa::MklPardisoSolver mass;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
auto model = fesa::AnalysisModel::Create(domain.Value());
|
||||
auto dofs = fesa::DofManager::Create(model.Value());
|
||||
auto state =
|
||||
fesa::SolidDynamicAnalysisState::Create(domain.Value(), dofs.Value());
|
||||
auto status = fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.ExecuteInto(domain.Value(), state.Value());
|
||||
if (!status.IsOk()) {
|
||||
const auto& e = state.Value().IterationRecords().back().evidence;
|
||||
std::cout << "Rigid-force failure worst DOF " << e.worst_full_dof
|
||||
<< " force ratio " << e.residual_metric
|
||||
<< " terms r,Ma,f,F,scale ";
|
||||
for (auto term : e.worst_force_terms) std::cout << term << ' ';
|
||||
std::cout << '\n';
|
||||
for (const auto& rec : state.Value().IterationRecords()) {
|
||||
if (rec.evidence.iteration_index < 10) {
|
||||
std::cout << "Trace " << rec.increment_index << ' '
|
||||
<< rec.evidence.iteration_index << ' ' << rec.outcome
|
||||
<< " ratio=" << rec.evidence.residual_metric
|
||||
<< " worst=" << rec.evidence.worst_full_dof
|
||||
<< " correction=" << rec.evidence.correction_metrics[0]
|
||||
<< '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(status.IsOk()) << solid_hht_test::Message(status);
|
||||
EXPECT_TRUE(std::any_of(
|
||||
state.Value().IterationRecords().begin(),
|
||||
state.Value().IterationRecords().end(),
|
||||
[](const auto& record) { return record.outcome == "paired-candidate"; }));
|
||||
const double roundoff = 512 * std::numeric_limits<double>::epsilon();
|
||||
for (const auto& f : state.Value().AcceptedFrames())
|
||||
for (unsigned n = 0; n < 8; ++n) {
|
||||
EXPECT_NEAR(f.displacement[3 * n], .5 * f.time * f.time,
|
||||
roundoff * .5 * f.time * f.time);
|
||||
EXPECT_NEAR(f.velocity[3 * n], f.time, roundoff * f.time);
|
||||
EXPECT_NEAR(f.acceleration[3 * n], 1, roundoff);
|
||||
for (unsigned c = 1; c < 3; ++c) {
|
||||
EXPECT_NEAR(f.displacement[3 * n + c], 0,
|
||||
roundoff * .5 * f.time * f.time);
|
||||
EXPECT_NEAR(f.velocity[3 * n + c], 0, roundoff * f.time);
|
||||
EXPECT_NEAR(f.acceleration[3 * n + c], 0, roundoff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal,
|
||||
PairedNewtonKeepsAllOriginalComponentsAcrossAxesAndScales) {
|
||||
const double roundoff = 512 * std::numeric_limits<double>::epsilon();
|
||||
for (unsigned axis = 0; axis < 3; ++axis)
|
||||
for (double acceleration : {1e-4, 1.0, 1000.0}) {
|
||||
SCOPED_TRACE(axis);
|
||||
SCOPED_TRACE(acceleration);
|
||||
auto d = solid_hht_test::Brick(.01, 8);
|
||||
d.dynamic_step->loads = {
|
||||
{"ALL", static_cast<int>(axis + 1), .25 * acceleration, {}}};
|
||||
auto domain = fesa::Domain::Create(std::move(d));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
fesa::SerialParallelFor parallel;
|
||||
fesa::MklPardisoSolver mass;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
auto model = fesa::AnalysisModel::Create(domain.Value());
|
||||
ASSERT_TRUE(model.HasValue());
|
||||
auto dofs = fesa::DofManager::Create(model.Value());
|
||||
ASSERT_TRUE(dofs.HasValue());
|
||||
auto state =
|
||||
fesa::SolidDynamicAnalysisState::Create(domain.Value(), dofs.Value());
|
||||
ASSERT_TRUE(state.HasValue());
|
||||
const auto status =
|
||||
fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.ExecuteInto(domain.Value(), state.Value());
|
||||
if (!status.IsOk()) {
|
||||
const auto& records = state.Value().IterationRecords();
|
||||
for (const auto& record : records) {
|
||||
if (record.increment_index != records.back().increment_index ||
|
||||
(record.evidence.iteration_index > 8 &&
|
||||
record.evidence.iteration_index < 95))
|
||||
continue;
|
||||
std::cout << "Rigid paired Newton trace "
|
||||
<< record.evidence.iteration_index << ' ' << record.outcome
|
||||
<< " metric=" << record.evidence.residual_metric
|
||||
<< " worst=" << record.evidence.worst_full_dof << " terms=";
|
||||
for (double term : record.evidence.worst_force_terms)
|
||||
std::cout << term << ' ';
|
||||
std::cout << " corrections=";
|
||||
for (double metric : record.evidence.correction_metrics)
|
||||
std::cout << metric << ' ';
|
||||
std::cout << '\n';
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(status.IsOk()) << solid_hht_test::Message(status);
|
||||
for (const auto& frame : state.Value().AcceptedFrames()) {
|
||||
EXPECT_LE(frame.iterations.back().residual_metric, 1e-10);
|
||||
EXPECT_LE(frame.iterations.back().update_metric, 1e-12);
|
||||
for (unsigned node = 0; node < 8; ++node)
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const double a = c == axis ? acceleration : 0;
|
||||
EXPECT_NEAR(frame.displacement[3 * node + c],
|
||||
.5 * a * frame.time * frame.time,
|
||||
roundoff * .5 * acceleration * frame.time * frame.time);
|
||||
EXPECT_NEAR(frame.velocity[3 * node + c], a * frame.time,
|
||||
roundoff * acceleration * frame.time);
|
||||
EXPECT_NEAR(frame.acceleration[3 * node + c], a,
|
||||
roundoff * acceleration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal,
|
||||
ActualTwoBrickLinearAndRotatingSmallStrainTlAreSecondOrder) {
|
||||
for (bool nonlinear : {false, true}) {
|
||||
SCOPED_TRACE(nonlinear);
|
||||
const auto coarse = ManufacturedRun(.04, 30, nonlinear);
|
||||
const auto medium = ManufacturedRun(.02, 60, nonlinear);
|
||||
const auto fine = ManufacturedRun(.01, 120, nonlinear);
|
||||
for (unsigned j = 0; j < 3; ++j) {
|
||||
const double p1 = std::log(coarse[j] / medium[j]) / std::log(2.0);
|
||||
const double p2 = std::log(medium[j] / fine[j]) / std::log(2.0);
|
||||
std::cout << "Hex8 " << (nonlinear ? "TL" : "linear") << " u/v/a channel "
|
||||
<< j << " errors " << coarse[j] << ' ' << medium[j] << ' '
|
||||
<< fine[j] << " orders " << p1 << ' ' << p2 << '\n';
|
||||
EXPECT_GT(fine[j],
|
||||
1e-8); // Meaningful truncation interval, away from noise.
|
||||
EXPECT_LT(coarse[j], .1);
|
||||
EXPECT_GE(p1, 1.8);
|
||||
EXPECT_GE(p2, 1.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
|
||||
// Node0's three translations are free. Exact polynomial integrals yield
|
||||
// M=(rho/27)I, Kii=(lambda+4mu)/9, Kij=(lambda+mu)/12.
|
||||
std::array<std::array<double, 3>, 3> errors{};
|
||||
for (unsigned level = 0; level < 3; ++level) {
|
||||
const double h = .02 / static_cast<double>(1U << level);
|
||||
const unsigned count = 32U << level;
|
||||
auto d = solid_hht_test::Brick(h, count);
|
||||
for (unsigned n = 1; n < 8; ++n)
|
||||
d.dynamic_step->boundaries.push_back(
|
||||
{"N" + std::to_string(n), 1, 3, 0, {}});
|
||||
d.dynamic_step->loads = {
|
||||
{"N0", 1, .01, {}}, {"N0", 2, .02, {}}, {"N0", 3, .03, {}}};
|
||||
auto domain = fesa::Domain::Create(std::move(d));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
fesa::SerialParallelFor parallel;
|
||||
fesa::MklPardisoSolver mass;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
auto result =
|
||||
fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.Execute(domain.Value());
|
||||
ASSERT_TRUE(result.HasValue())
|
||||
<< solid_hht_test::Message(result.GetStatus());
|
||||
const auto& f = result.Value().state.CommittedState();
|
||||
const double m = 2.0 / 27;
|
||||
const double diagonal = (4.8 + 4 * 4.8) / 9;
|
||||
const double off = (4.8 + 4.8) / 12;
|
||||
const std::array<double, 2> k{diagonal + 2 * off, diagonal - off};
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
std::array<double, 3> truth{};
|
||||
const std::array<double, 2> force{.02, .01 * (c + 1) - .02};
|
||||
for (unsigned mode = 0; mode < 2; ++mode) {
|
||||
const double w = std::sqrt(k[mode] / m);
|
||||
truth[0] += force[mode] / k[mode] * (1 - std::cos(w * f.time));
|
||||
truth[1] += force[mode] / (m * w) * std::sin(w * f.time);
|
||||
truth[2] += force[mode] / m * std::cos(w * f.time);
|
||||
}
|
||||
const std::array<double, 3> actual{f.displacement[c], f.velocity[c],
|
||||
f.acceleration[c]};
|
||||
for (unsigned j = 0; j < 3; ++j)
|
||||
errors[level][j] += std::pow(actual[j] - truth[j], 2);
|
||||
}
|
||||
}
|
||||
for (unsigned j = 0; j < 3; ++j) {
|
||||
const double p1 =
|
||||
.5 * std::log(errors[0][j] / errors[1][j]) / std::log(2.0);
|
||||
const double p2 =
|
||||
.5 * std::log(errors[1][j] / errors[2][j]) / std::log(2.0);
|
||||
std::cout << "Independent MDOF channel " << j << " orders " << p1 << ' '
|
||||
<< p2 << '\n';
|
||||
EXPECT_GE(p1, 1.8);
|
||||
EXPECT_GE(p2, 1.8);
|
||||
EXPECT_GT(errors[2][j], 1e-15);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal, IndependentScalarBrickOscillatorHasSecondOrderUVA) {
|
||||
std::array<std::array<double, 3>, 3> errors{};
|
||||
const double m = 2.0 / 27;
|
||||
const double k = (4.8 + 4 * 4.8) / 9;
|
||||
const double w = std::sqrt(k / m);
|
||||
for (unsigned level = 0; level < 3; ++level) {
|
||||
const double h = .02 / static_cast<double>(1U << level);
|
||||
auto d = solid_hht_test::Brick(h, 32U << level);
|
||||
d.dynamic_step->boundaries.push_back({"N0", 2, 3, 0, {}});
|
||||
for (unsigned n = 1; n < 8; ++n)
|
||||
d.dynamic_step->boundaries.push_back(
|
||||
{"N" + std::to_string(n), 1, 3, 0, {}});
|
||||
d.dynamic_step->loads = {{"N0", 1, .01, {}}};
|
||||
auto domain = fesa::Domain::Create(std::move(d));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
fesa::SerialParallelFor parallel;
|
||||
fesa::MklPardisoSolver mass;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
auto result =
|
||||
fesa::SolidHhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.Execute(domain.Value());
|
||||
ASSERT_TRUE(result.HasValue())
|
||||
<< solid_hht_test::Message(result.GetStatus());
|
||||
const auto& f = result.Value().state.CommittedState();
|
||||
const std::array<double, 3> truth{.01 / k * (1 - std::cos(w * f.time)),
|
||||
.01 / (m * w) * std::sin(w * f.time),
|
||||
.01 / m * std::cos(w * f.time)};
|
||||
const std::array<double, 3> actual{f.displacement[0], f.velocity[0],
|
||||
f.acceleration[0]};
|
||||
for (unsigned j = 0; j < 3; ++j)
|
||||
errors[level][j] = std::abs(actual[j] - truth[j]);
|
||||
EXPECT_NEAR(
|
||||
f.kinetic_energy, .5 * m * f.velocity[0] * f.velocity[0],
|
||||
512 * std::numeric_limits<double>::epsilon() * f.kinetic_energy);
|
||||
EXPECT_NEAR(f.strain_energy, .5 * k * f.displacement[0] * f.displacement[0],
|
||||
512 * std::numeric_limits<double>::epsilon() * f.strain_energy);
|
||||
}
|
||||
for (unsigned j = 0; j < 3; ++j) {
|
||||
const double p1 = std::log(errors[0][j] / errors[1][j]) / std::log(2.0);
|
||||
const double p2 = std::log(errors[1][j] / errors[2][j]) / std::log(2.0);
|
||||
std::cout << "Independent SDOF channel " << j << " orders " << p1 << ' '
|
||||
<< p2 << '\n';
|
||||
EXPECT_GE(p1, 1.8);
|
||||
EXPECT_GE(p2, 1.8);
|
||||
EXPECT_GT(errors[2][j], 1e-8);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal,
|
||||
BendingMeshRefinementQuantifiesFullIntegrationShearLocking) {
|
||||
constexpr double thickness = .125;
|
||||
constexpr double curvature = .01;
|
||||
constexpr double lambda = 4.8;
|
||||
constexpr double mu = 4.8;
|
||||
const double continuum = .5 * (lambda + 2 * mu) * curvature * curvature *
|
||||
thickness * thickness * thickness / 12;
|
||||
double previous_error = 0;
|
||||
for (unsigned nx : {1U, 2U, 4U, 8U}) {
|
||||
double numerical = 0;
|
||||
for (unsigned e = 0; e < nx; ++e) {
|
||||
auto d = solid_hht_test::Brick();
|
||||
fesa::Hex8Element::Coordinates x{};
|
||||
fesa::ElementDofLayout layout{
|
||||
{"I", e + 1, std::to_string(e + 1)},
|
||||
{0, 1, 2, 3, 4, 5, 6, 7},
|
||||
{fesa::DofComponent::kUx, fesa::DofComponent::kUy,
|
||||
fesa::DofComponent::kUz}};
|
||||
fesa::Vector u(24);
|
||||
for (unsigned a = 0; a < 8; ++a) {
|
||||
x[a] = {static_cast<double>(e) / nx + d.nodes[a].coordinates[0] / nx,
|
||||
thickness * (d.nodes[a].coordinates[1] - .5),
|
||||
d.nodes[a].coordinates[2]};
|
||||
u[3 * a] = -curvature * x[a][0] * x[a][1];
|
||||
u[3 * a + 1] = .5 * curvature * x[a][0] * x[a][0];
|
||||
}
|
||||
auto element = fesa::Hex8Element::Create(e, layout, x, d.materials[0], 2);
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
auto response = element.Value().EvaluateLinear(u);
|
||||
ASSERT_TRUE(response.HasValue());
|
||||
numerical += response.Value().strain_energy;
|
||||
}
|
||||
const double error = numerical / continuum - 1;
|
||||
const double exact_parasitic =
|
||||
mu / (lambda + 2 * mu) / std::pow(nx * thickness, 2);
|
||||
EXPECT_NEAR(error, exact_parasitic, 2e-11);
|
||||
if (previous_error != 0) EXPECT_NEAR(previous_error / error, 4, 1e-10);
|
||||
std::cout << "Bending nx=" << nx
|
||||
<< " energy/continuum=" << numerical / continuum << '\n';
|
||||
previous_error = error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef FESA_TESTS_SOLID_HHT_TEST_FIXTURE_H_
|
||||
#define FESA_TESTS_SOLID_HHT_TEST_FIXTURE_H_
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/elements/hex8_definition.h"
|
||||
#include "fesa/model/domain.h"
|
||||
#include "fesa/properties/solid_section.h"
|
||||
|
||||
namespace solid_hht_test {
|
||||
inline fesa::ModelDefinition Brick(double h = 0.01, unsigned count = 20,
|
||||
bool nonlinear = false) {
|
||||
fesa::ModelDefinition d;
|
||||
d.source_path = "solid-hht-test.inp";
|
||||
const std::array<std::array<double, 3>, 8> x{{{0, 0, 0},
|
||||
{1, 0, 0},
|
||||
{1, 1, 0},
|
||||
{0, 1, 0},
|
||||
{0, 0, 1},
|
||||
{1, 0, 1},
|
||||
{1, 1, 1},
|
||||
{0, 1, 1}}};
|
||||
for (unsigned i = 0; i < 8; ++i) {
|
||||
d.nodes.push_back({{"I", i + 1, std::to_string(i + 1)}, x[i], {}});
|
||||
d.node_sets.push_back({"N" + std::to_string(i), {}, {i}, {}});
|
||||
}
|
||||
d.node_sets.push_back({"ALL", {}, {0, 1, 2, 3, 4, 5, 6, 7}, {}});
|
||||
d.materials = {{"M", 12.0, 0.25, {}}};
|
||||
d.densities = {{0, 2.0, {}}};
|
||||
d.solid_sections.emplace_back(fesa::SourceEntityId{"I", 1, "1"}, "E", 0,
|
||||
fesa::SourceLocation{});
|
||||
d.solid_elements.emplace_back(
|
||||
fesa::SourceEntityId{"I", 8, "8"},
|
||||
std::array<fesa::EntityIndex, 8>{0, 1, 2, 3, 4, 5, 6, 7}, 0, 0,
|
||||
fesa::SourceLocation{});
|
||||
fesa::DynamicStepDefinition step;
|
||||
step.source_step_name_raw = "Dynamic";
|
||||
step.source_step_name_canonical = "DYNAMIC";
|
||||
step.initial_increment = h;
|
||||
step.time_period = static_cast<double>(count) * h;
|
||||
step.increment_count = count;
|
||||
step.maximum_increments = count;
|
||||
step.nlgeom = nonlinear;
|
||||
d.dynamic_step = step;
|
||||
return d;
|
||||
}
|
||||
inline std::string Message(const fesa::Status& s) {
|
||||
return s.Diagnostics().empty() ? "" : s.Diagnostics()[0].message;
|
||||
}
|
||||
} // namespace solid_hht_test
|
||||
#endif
|
||||
Reference in New Issue
Block a user