diff --git a/docs/calculation-performance/t4-shell-optimization.md b/docs/calculation-performance/t4-shell-optimization.md index cb8e908..5043bb3 100644 --- a/docs/calculation-performance/t4-shell-optimization.md +++ b/docs/calculation-performance/t4-shell-optimization.md @@ -230,3 +230,60 @@ Source commit c397a147b0e885384da8dee81ef645b9d0b17adb에서 corrected-prefix cl Release exe SHA7d3c48351a8b76dbc5274468fc9f205386d28521cde79633dcd970a861321143. Build/native/parent PATH, environment/identity 전후 및 timer facts는 parent constant-shell-native.json/development-constant-shell-before.json/development-constant-shell-after.json/development-constant-shell.json과 해당 run-1/native.json/run.json/stdout/solver log에 보존한다. 최종 HDF5가 없어서 reader/precheck와 blocking U comparator 및 UR warning 비교는 모두 미실행이다. 729node/101frame/schema2 또는 required reference PASS로 표시하지 않는다. 기존 원본이나 최종 H5를 수정하지 않고 현재 partial 파일과 timeout 자료를 보존했다. 추가 micro/profile/whole-run 반복은 하지 않았다. 다음 우선 후보는 profile에서 큰 EvaluateAssembly/EvaluateConstraintOperators 중복을 줄이는 exact true-trial internal constraint subset 재사용이다. False 위치/root 경로 및 external force/rate/acceleration primitive는 유지한다. 구체 context/q/v/direction/request/gate/lifetime 계획과 최소 private friend 범위를 먼저 검토한다. Node-local support6 AD는 support/비용 근거가 충분해진 경우의 비교 후보이며 현재 구현 범위를 확대하지 않는다. + +## 완성된 true trial의 내부 constraint 자료 재사용 + +이 후보는 기존 SW profile의 Assembly/ConstraintOperators 중복 경로를 근거로 한다. +완성된 true assembly가 이미 계산한 constraint value4, gradient4×24, Hessian24×24 +4개와 방향3차 gradient24 4개만 trial-owned optional에 보관한다. 기존 internal, +inertia, ordered external/recovery/finite/CSR/hidden-gradient gate가 모두 성공한 +마지막 위치에서 기존 자료를 move한다. False와 initial frame은 저장하지 않는다. +Accepted frame, reduced history, HDF5에는 이 자료가 붙지 않아 증분마다 누적되는 +구조가 아니다. 기존 Matrix/Vector deep-copy와 move 수명을 사용한다. + +소비자는 기존 inventory/null/transverse/node-row gate 뒤의 원래 internal 평가 +자리에서만 재사용을 확인한다. 구성64개 값과 source identity, element/scatter, +node IDs, q/v, time/frame/increment/iteration 및 directional 요청이 일치해야 한다. +현재/저장 q/v·구성·time은 finite 확인 뒤 representation 비교하여 ±0도 구별한다. +Worker 평가 전후와 소비 thread의 masked FP control 및 기존 scope 내부 BLAS1, +dynamic0이 안정적인 현재 Intel-thread 빌드에 한정한다. Private metadata getter는 +전역 policy를 바꾸지 않고 기존 caller TLS를 원복한다. 불일치/비유한/불명확 key, +shape/payload 오류는 그 자리의 원래 full 평가와 Status로 돌아간다. 외부 force, +rate/acceleration primitive, 누적 순서, physics/gates/diagnostic은 그대로 수행한다. + +Private element configuration header와 public header의 forward/private friend만 +추가했다. Element가 analysis에 의존하지 않고 vendor 타입을 public에 노출하지 않는다. +Density는 내부 네 자료의 함수/energy 입력이 아니고 inertia는 매번 평가하므로 키에서 +제외한다. 약13.59MiB는676 shell의 payload 일부 산술 추정이며 객체/allocator/source +문자열/추가 metadata 및 실제 peak 메모리 실측을 포함하는 상한이 아니다. + +독립 source 검토는 최초 bit-only key가 동일 NaN/Inf current+saved key를 수용해 +원래 실패를 생략할 수 있음을 발견했다(needs-fix). 해당 자료와 manifest를 보존한 뒤 +finite key miss를 추가하여 재검토 PASS를 받았다. Source-derived 반례의 native RED는 +미실행이며 실제 RED 관측이라고 기록하지 않는다. 최초 Debug build는 새 raw 자료 +fixture의 Vector3 read-only component 대입 오류로 cmake native2/외부 PowerShell1, +13.2472474초에 실패했다. 원래 세 좌표를 생성자로 대입하는 test-only 수정으로 +production hash를 유지했고 실패 로그/native/source 자료도 보존했다. + +수정 source manifest C4EBD4169D44918E56DECDB0AB1D4AAFABDBA866612874C788DD883CF460C266에서 +정식 Debug 재빌드7 edges native0/6.30초, 새 cache/getter6/6 PASS(native0, +CTest11.04초/native11.08초), 기존 reduced/initial Newton27/27 PASS(native0, +CTest6.40초/native6.43초)를 확인했다. 실제 CTest 합33개이며 기존 Constant의33-case +oracle와 다른 수다. Jet 수식/저장/provenance와 기존 element scope body는 바뀌지 +않아 그 oracle는 이번 슬롯에서 반복하지 않았다. Raw 네 자료/forced-fresh의 strict +bits, deep copy/move, actual FP/TLS/dynamic miss, serial/TBB, 외부 rates, 동일 +NaN/±Inf q/v key의 실패 진단 전필드와 비유한 time의 fresh 성공을 검사했다. + +Corrected-prefix 실제 Ninja deps는 assembly41/reduced operators42/shell26이며 +모두 새 private configuration header를 포함한다. Library SHA +5108045D88A2439E57934B8D66317DE1B545AFE122BFCF082D60DDB732D20DFE, +unit SHA CF6D180565FCFDF5AD19EEA26EF627A38C5C8603AE331607031411828700F8BD다. +Parent artifact의 true-trial-source-vector3.json/patch, +true-trial-vector3-debug-native.json/built-hashes.json 및 단계별 로그에 실제 +source/compiler/argv/cwd/native/deps/thread/hook을 보존했다. Source10 hash 일치와 +관련 프로세스0 확인 후 CPU를 반환했다. + +현재 cache 후보의 Release/원본 shell 실행/실제 call 감소/비용 및 전체 Debug, +reference/C08/최종100초 합격은 미실행이다. 부분 실행 자료나 이전 source의 PASS를 +이 후보의 전체 성능·물리 검증 완료로 바꾸지 않는다. 다음 bounded 개발 실행은 +0warmup/1회만 수행하고 결과에 따라 다음 최소 후보를 검토한다. diff --git a/include/fesa/elements/nonlinear_mitc4_shell.h b/include/fesa/elements/nonlinear_mitc4_shell.h index 9e37076..4359400 100644 --- a/include/fesa/elements/nonlinear_mitc4_shell.h +++ b/include/fesa/elements/nonlinear_mitc4_shell.h @@ -15,6 +15,10 @@ namespace fesa { +namespace nonlinear_mitc4_internal { +struct ConfigurationSnapshot; +} // namespace nonlinear_mitc4_internal + /// @brief Stores one four-node nonlinear shell trial in global coordinates. struct NonlinearMitc4TrialDofs { std::array displacement; @@ -129,6 +133,8 @@ class NonlinearMitc4Shell { const Vector3& force_acceleration) const; private: + friend struct nonlinear_mitc4_internal::ConfigurationSnapshot; + /// @brief Retains validated fixed-reference data without analysis ownership. NonlinearMitc4Shell(std::array coordinates, std::array initial_frames, diff --git a/src/fesa/analysis/hht_dynamic_assembly.cpp b/src/fesa/analysis/hht_dynamic_assembly.cpp index e0ba448..bc60030 100644 --- a/src/fesa/analysis/hht_dynamic_assembly.cpp +++ b/src/fesa/analysis/hht_dynamic_assembly.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -6,6 +7,10 @@ #include #include +#if defined(_M_X64) +#include +#endif + #include "analysis/hht_dynamic_internal.h" #include "math/dense_blas_internal.h" #include "fesa/assembly/parallel_for.h" @@ -14,6 +19,29 @@ #include "fesa/model/source_target_resolver.h" namespace fesa::hht_internal { + +TrialEvaluationContext ReadTrialEvaluationContext() noexcept { + TrialEvaluationContext result{}; +#if defined(_M_X64) + result.rounding = std::fegetround(); + result.mxcsr_control = _mm_getcsr() & 0xFFC0U; + result.dense_policy = dense_blas_internal::ReadScopedLocalDensePolicy(); + result.supported = result.rounding >= 0; +#endif + return result; +} + +bool ReusableTrialContext(const TrialEvaluationContext& before, + const TrialEvaluationContext& after) noexcept { + return before.supported && after.supported && + before.rounding == after.rounding && + before.mxcsr_control == after.mxcsr_control && + (before.mxcsr_control & 0x1F80U) == 0x1F80U && + before.dense_policy.blas_max_threads == 1 && + after.dense_policy.blas_max_threads == 1 && + before.dense_policy.dynamic == 0 && after.dense_policy.dynamic == 0; +} + namespace { /// @brief Reports a dynamic assembly or equilibrium failure. @@ -388,6 +416,10 @@ Result EvaluateAssembly( std::vector> inertia( shells.size()); std::vector> failures(shells.size()); + const bool retain_constraints = + directional_third && !frame.is_initial && std::isfinite(frame.time); + std::vector evaluation_contexts( + retain_constraints ? shells.size() : 0U); parallel_for.Execute(shells.size(), [&](const std::size_t e) { const auto q = Gather(frame.generalized_coordinates, shells[e]); const auto v = Gather(frame.velocity, shells[e]); @@ -397,12 +429,18 @@ Result EvaluateAssembly( trial.displacement[node] = Triple(q, 6U * node); trial.rotation_vector[node] = Triple(q, 6U * node + 3U); } + const auto before = retain_constraints ? ReadTrialEvaluationContext() + : TrialEvaluationContext{}; auto f = EvaluateInternalResidualAndTangent( shells[e].internal, trial, directional_third ? &v : nullptr); if (!f.HasValue()) { failures[e] = f.GetStatus(); return; } + if (retain_constraints) { + const auto after = ReadTrialEvaluationContext(); + if (ReusableTrialContext(before, after)) evaluation_contexts[e] = after; + } auto m = shells[e].inertia.Evaluate(trial, v, a); if (!m.HasValue()) { failures[e] = m.GetStatus(); @@ -605,13 +643,52 @@ Result EvaluateAssembly( return Result::Failure(gradient.GetStatus()); hidden_gradient = std::move(gradient.Value()); } + std::optional trial_constraints; + if (retain_constraints) { + TrialConstraintData data{}; + data.source_path = domain.SourcePath(); + data.time = frame.time; + data.frame_index = frame.frame_index; + data.increment_index = frame.increment_index; + data.iteration_count = frame.iteration_count; + data.shells.resize(shells.size()); + for (std::size_t e = 0U; e < shells.size(); ++e) { + if (!ReusableTrialContext(evaluation_contexts[e], evaluation_contexts[e])) + continue; + auto& record = data.shells[e]; + record.element_index = shells[e].element_index; + record.scatter = shells[e].scatter; + record.configuration = + nonlinear_mitc4_internal::ConfigurationSnapshot::Read( + shells[e].internal); + record.evaluation_context = evaluation_contexts[e]; + for (std::size_t i = 0U; i < 24U; ++i) { + record.q[i] = frame.generalized_coordinates[shells[e].scatter[i]]; + record.v[i] = frame.velocity[shells[e].scatter[i]]; + } + for (std::size_t node = 0U; node < 4U; ++node) + record.node_source_ids[node] = + domain.Nodes()[shells[e].scatter[6U * node] / 6U].source_id; + auto& response = *internal[e]; + record.drilling_constraint = std::move(response.drilling_constraint); + record.drilling_constraint_gradient = + std::move(response.drilling_constraint_gradient); + record.drilling_constraint_hessian = + std::move(response.drilling_constraint_hessian); + record.drilling_constraint_third_directional_gradient = + std::move(response.drilling_constraint_third_directional_gradient); + record.directional_internal = true; + } + trial_constraints = std::move(data); + } return Result::Success( {std::move(frame), std::move(mass.Value()), std::move(position.Value()), std::move(velocity.Value()), std::move(stiffness.Value()), std::move(effective.Value()), std::move(metric.Value()), std::move(third), std::move(third_scale), std::move(rotations), momentum_scale, angular_scale, std::move(hidden_second), - std::move(hidden_second_scale), std::move(hidden_gradient)}); + std::move(hidden_second_scale), std::move(hidden_gradient), + std::move(trial_constraints)}); } Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs, diff --git a/src/fesa/analysis/hht_dynamic_internal.h b/src/fesa/analysis/hht_dynamic_internal.h index da59a70..c199e93 100644 --- a/src/fesa/analysis/hht_dynamic_internal.h +++ b/src/fesa/analysis/hht_dynamic_internal.h @@ -2,6 +2,8 @@ #define FESA_ANALYSIS_HHT_DYNAMIC_INTERNAL_H_ #include +#include +#include #include #include @@ -10,6 +12,8 @@ #include "fesa/elements/nonlinear_mitc4_inertia.h" #include "fesa/elements/nonlinear_mitc4_shell.h" #include "fesa/math/sparse_matrix.h" +#include "elements/nonlinear_mitc4_configuration_internal.h" +#include "math/dense_blas_internal.h" namespace fesa { class LinearSolver; @@ -25,6 +29,49 @@ struct RuntimeShell { NonlinearMitc4Inertia inertia; }; +/// @brief Captures only controls used by the current MSVC x64 local evaluator. +struct TrialEvaluationContext { + int rounding{-1}; + unsigned int mxcsr_control{0U}; + dense_blas_internal::LocalDensePolicy dense_policy{0, -1}; + bool supported{false}; +}; + +/// @brief Queries metadata without changing FP controls or the restored TLS. +TrialEvaluationContext ReadTrialEvaluationContext() noexcept; + +/// @brief Allows only masked, stable, single-worker nondynamic local policy. +bool ReusableTrialContext(const TrialEvaluationContext& before, + const TrialEvaluationContext& after) noexcept; + +/// @brief Owns only complete directional constraint data for one local trial. +struct DirectionalShellConstraintData { + EntityIndex element_index{0U}; + std::array scatter{}; + std::array node_source_ids{}; + nonlinear_mitc4_internal::ConfigurationSnapshot configuration; + std::array q{}; + std::array v{}; + TrialEvaluationContext evaluation_context; + bool directional_internal{false}; + Vector drilling_constraint{0U}; + Matrix drilling_constraint_gradient{0U, 0U}; + std::array drilling_constraint_hessian{ + Matrix{0U, 0U}, Matrix{0U, 0U}, Matrix{0U, 0U}, Matrix{0U, 0U}}; + std::array drilling_constraint_third_directional_gradient{ + Vector{0U}, Vector{0U}, Vector{0U}, Vector{0U}}; +}; + +/// @brief Travels with one complete assembly, never accepted frame/history. +struct TrialConstraintData { + std::filesystem::path source_path; + double time{0.0}; + std::uint32_t frame_index{0U}; + std::uint32_t increment_index{0U}; + std::uint32_t iteration_count{0U}; + std::vector shells; +}; + /// @brief Stores a complete trial with independently assembled operators. struct DynamicAssembly { DynamicAcceptedFrameState frame; @@ -43,6 +90,7 @@ struct DynamicAssembly { Vector hidden_second_scale{0U}; // Canonical scalar drilling-constraint derivative, indexed by global node. std::optional hidden_gradient; + std::optional trial_constraints; }; /// @brief Builds checked shell evaluators in stable source order. diff --git a/src/fesa/analysis/hht_reduced_operators.cpp b/src/fesa/analysis/hht_reduced_operators.cpp index 3ff4cfd..a34cb5a 100644 --- a/src/fesa/analysis/hht_reduced_operators.cpp +++ b/src/fesa/analysis/hht_reduced_operators.cpp @@ -60,6 +60,88 @@ Vector Local(const Vector& full, return local; } +/// @brief Preserves the exact floating representation, including signed zero. +bool SameBits(const double left, const double right) { + return std::memcmp(&left, &right, sizeof(double)) == 0; +} + +/// @brief Compares semantic and original source identity without object keys. +bool SameSource(const SourceEntityId& left, const SourceEntityId& right) { + return left.instance_name == right.instance_name && + left.source_label == right.source_label && + left.source_label_text == right.source_label_text; +} + +/// @brief Finds only the current complete directional response, otherwise miss. +const hht_internal::DirectionalShellConstraintData* MatchingConstraints( + const Domain& domain, const hht_internal::RuntimeShell& shell, + const hht_internal::DynamicAssembly& assembly, const std::size_t index, + const std::size_t shell_count, const Vector& q, const Vector& v) { + if (assembly.frame.is_initial || !assembly.trial_constraints.has_value()) + return nullptr; + const auto& owner = *assembly.trial_constraints; + const auto& frame = assembly.frame; + if (owner.shells.size() != shell_count || index >= owner.shells.size() || + owner.source_path != domain.SourcePath() || + !std::isfinite(owner.time) || !std::isfinite(frame.time) || + !SameBits(owner.time, frame.time) || + owner.frame_index != frame.frame_index || + owner.increment_index != frame.increment_index || + owner.iteration_count != frame.iteration_count) + return nullptr; + const auto& record = owner.shells[index]; + if (!record.directional_internal || + record.element_index != shell.element_index || + record.scatter != shell.scatter || + !hht_internal::ReusableTrialContext( + record.evaluation_context, + hht_internal::ReadTrialEvaluationContext())) + return nullptr; + const auto configuration = + nonlinear_mitc4_internal::ConfigurationSnapshot::Read(shell.internal); + if (record.configuration.source.file != configuration.source.file || + record.configuration.source.line != configuration.source.line || + record.configuration.identity != configuration.identity) + return nullptr; + for (std::size_t i = 0U; i < configuration.values.size(); ++i) + if (!std::isfinite(record.configuration.values[i]) || + !std::isfinite(configuration.values[i]) || + !SameBits(record.configuration.values[i], configuration.values[i])) + return nullptr; + for (std::size_t i = 0U; i < 24U; ++i) + if (!std::isfinite(record.q[i]) || !std::isfinite(q[i]) || + !std::isfinite(record.v[i]) || !std::isfinite(v[i]) || + !SameBits(record.q[i], q[i]) || !SameBits(record.v[i], v[i])) + return nullptr; + for (std::size_t node = 0U; node < 4U; ++node) { + const auto global_node = shell.scatter[6U * node] / 6U; + if (global_node >= domain.Nodes().size() || + !SameSource(record.node_source_ids[node], + domain.Nodes()[global_node].source_id)) + return nullptr; + } + if (record.drilling_constraint.Size() != 4U || + record.drilling_constraint_gradient.Rows() != 4U || + record.drilling_constraint_gradient.Columns() != 24U) + return nullptr; + for (std::size_t node = 0U; node < 4U; ++node) { + const auto& hessian = record.drilling_constraint_hessian[node]; + const auto& third = + record.drilling_constraint_third_directional_gradient[node]; + if (hessian.Rows() != 24U || hessian.Columns() != 24U || + third.Size() != 24U || !std::isfinite(record.drilling_constraint[node])) + return nullptr; + for (std::size_t i = 0U; i < 24U; ++i) { + if (!std::isfinite(record.drilling_constraint_gradient(node, i)) || + !std::isfinite(third[i])) + return nullptr; + for (std::size_t j = 0U; j < 24U; ++j) + if (!std::isfinite(hessian(i, j))) return nullptr; + } + } + return &record; +} + NonlinearMitc4TrialDofs Trial(const Vector& local) { NonlinearMitc4TrialDofs trial{}; for (std::size_t node = 0U; node < 4U; ++node) { @@ -443,32 +525,46 @@ Result EvaluateConstraintOperators( node_row[chart.null_nodes[row]] = row; } std::vector external_done(domain.Nodes().size(), false); + std::size_t shell_index = 0U; for (const auto& shell : shells) { const auto q = Local(assembly.frame.generalized_coordinates, shell); const auto v = Local(assembly.frame.velocity, shell); - const auto internal = EvaluateInternalResidualAndTangent(shell.internal, - Trial(q), &v); - if (!internal.HasValue()) - return Result::Failure(internal.GetStatus()); + const auto* saved = MatchingConstraints(domain, shell, assembly, + shell_index++, shells.size(), q, v); + std::optional fresh; + if (saved == nullptr) { + auto internal = + EvaluateInternalResidualAndTangent(shell.internal, Trial(q), &v); + if (!internal.HasValue()) + return Result::Failure(internal.GetStatus()); + fresh = std::move(internal.Value()); + } + const auto& constraint = saved != nullptr ? saved->drilling_constraint + : fresh->drilling_constraint; + const auto& gradient_data = saved != nullptr + ? saved->drilling_constraint_gradient + : fresh->drilling_constraint_gradient; + const auto& third_data = + saved != nullptr + ? saved->drilling_constraint_third_directional_gradient + : fresh->drilling_constraint_third_directional_gradient; + const auto& hessian_data = saved != nullptr + ? saved->drilling_constraint_hessian + : fresh->drilling_constraint_hessian; for (std::size_t local_node = 0U; local_node < 4U; ++local_node) { const auto global_node = shell.scatter[6U * local_node] / 6U; if (global_node >= node_row.size() || node_row[global_node] == rows) continue; const auto row = node_row[global_node]; - result.rows[row].value += - internal.Value().drilling_constraint[local_node]; + result.rows[row].value += constraint[local_node]; for (std::size_t i = 0U; i < 24U; ++i) { const auto global_i = shell.scatter[i]; - const double gradient = - internal.Value().drilling_constraint_gradient(local_node, i); - const double third = internal.Value() - .drilling_constraint_third_directional_gradient - [local_node][i]; + const double gradient = gradient_data(local_node, i); + const double third = third_data[local_node][i]; if (gradient != 0.0) gradients[row][global_i] += gradient; if (third != 0.0) third_gradients[row][global_i] += third; for (std::size_t j = 0U; j < 24U; ++j) { - const double hessian = - internal.Value().drilling_constraint_hessian[local_node](i, j); + const double hessian = hessian_data[local_node](i, j); if (hessian != 0.0) hessians[row][{global_i, shell.scatter[j]}] += hessian; } diff --git a/src/fesa/elements/nonlinear_mitc4_configuration_internal.h b/src/fesa/elements/nonlinear_mitc4_configuration_internal.h new file mode 100644 index 0000000..618eebc --- /dev/null +++ b/src/fesa/elements/nonlinear_mitc4_configuration_internal.h @@ -0,0 +1,24 @@ +#ifndef FESA_SRC_FESA_ELEMENTS_NONLINEAR_MITC4_CONFIGURATION_INTERNAL_H_ +#define FESA_SRC_FESA_ELEMENTS_NONLINEAR_MITC4_CONFIGURATION_INTERNAL_H_ + +#include +#include + +#include "fesa/elements/nonlinear_mitc4_shell.h" + +namespace fesa::nonlinear_mitc4_internal { + +/// @brief Owns reference values without depending on an analysis or identity +/// key. +struct ConfigurationSnapshot { + std::array values{}; + SourceLocation source{}; + std::string identity; + + /// @brief Copies all immutable inputs of the internal equilibrium evaluator. + static ConfigurationSnapshot Read(const NonlinearMitc4Shell& element); +}; + +} // namespace fesa::nonlinear_mitc4_internal + +#endif // FESA_SRC_FESA_ELEMENTS_NONLINEAR_MITC4_CONFIGURATION_INTERNAL_H_ diff --git a/src/fesa/elements/nonlinear_mitc4_shell.cpp b/src/fesa/elements/nonlinear_mitc4_shell.cpp index c025157..e88ea3b 100644 --- a/src/fesa/elements/nonlinear_mitc4_shell.cpp +++ b/src/fesa/elements/nonlinear_mitc4_shell.cpp @@ -10,9 +10,34 @@ #include #include "fesa/elements/finite_rotation.h" +#include "elements/nonlinear_mitc4_configuration_internal.h" #include "math/dense_blas_internal.h" namespace fesa { + +nonlinear_mitc4_internal::ConfigurationSnapshot +nonlinear_mitc4_internal::ConfigurationSnapshot::Read( + const NonlinearMitc4Shell& element) { + ConfigurationSnapshot result{}; + std::size_t index = 0U; + for (const auto& coordinates : element.coordinates_) + for (std::size_t c = 0U; c < 3U; ++c) + result.values[index++] = coordinates[c]; + for (const auto& frame : element.initial_frames_) + for (std::size_t i = 0U; i < 3U; ++i) + for (std::size_t j = 0U; j < 3U; ++j) + result.values[index++] = frame(i, j); + for (const auto& director : element.initial_directors_) + for (std::size_t c = 0U; c < 3U; ++c) result.values[index++] = director[c]; + result.values[index++] = element.thickness_; + result.values[index++] = element.youngs_modulus_; + result.values[index++] = element.poisson_ratio_; + result.values[index] = element.drilling_stiffness_; + result.source = element.source_location_; + result.identity = element.identity_; + return result; +} + namespace { constexpr std::size_t kNodeCount = 4U; diff --git a/src/fesa/math/dense_blas_internal.cpp b/src/fesa/math/dense_blas_internal.cpp index a544715..ed5de9d 100644 --- a/src/fesa/math/dense_blas_internal.cpp +++ b/src/fesa/math/dense_blas_internal.cpp @@ -40,6 +40,11 @@ ScopedLocalDenseThreads::~ScopedLocalDenseThreads() { if (restore_default_) static_cast(mkl_set_num_threads_local(0)); } +LocalDensePolicy ReadScopedLocalDensePolicy() noexcept { + const ScopedLocalDenseThreads scope{}; + return {mkl_domain_get_max_threads(MKL_DOMAIN_BLAS), mkl_get_dynamic()}; +} + bool CholeskyLowerAndInverse(const Matrix& matrix, Matrix& lower, Matrix& inverse_lower, CholeskyFailure* failure) { if (failure != nullptr) *failure = CholeskyFailure::kNone; diff --git a/src/fesa/math/dense_blas_internal.h b/src/fesa/math/dense_blas_internal.h index ea3b107..b2d4481 100644 --- a/src/fesa/math/dense_blas_internal.h +++ b/src/fesa/math/dense_blas_internal.h @@ -1,10 +1,10 @@ #ifndef FESA_SRC_FESA_MATH_DENSE_BLAS_INTERNAL_H_ #define FESA_SRC_FESA_MATH_DENSE_BLAS_INTERNAL_H_ -#include - #include +#include + #include "fesa/core/status.h" #include "fesa/math/matrix.h" #include "fesa/math/vector.h" @@ -40,6 +40,16 @@ class ScopedLocalDenseThreads { bool restore_default_{false}; }; +/// @brief Records requested backend policy, without claiming actual threads. +struct LocalDensePolicy { + int blas_max_threads; + int dynamic; +}; + +/// @brief Reads BLAS policy inside the same restored scope as local elements. +/// @note No process-wide policy is changed; positive caller TLS is retained. +LocalDensePolicy ReadScopedLocalDensePolicy() noexcept; + /// @brief Converts a dense storage length to the private MKL integer type. /// @return The converted length or an uncategorized overflow failure. Result ToMklSize(std::size_t size); diff --git a/tests/unit/analysis/hht_reduced_shell_test.cpp b/tests/unit/analysis/hht_reduced_shell_test.cpp index 57f29a0..d8ce519 100644 --- a/tests/unit/analysis/hht_reduced_shell_test.cpp +++ b/tests/unit/analysis/hht_reduced_shell_test.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,6 +21,7 @@ #endif #include "analysis/hht_reduced_internal.h" +#include "math/dense_blas_internal.h" #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/hht_time_integration.h" #include "fesa/assembly/parallel_for.h" @@ -26,6 +29,71 @@ namespace { +class RestoreTrialEnvironment { + public: + RestoreTrialEnvironment() + : rounding_(std::fegetround()), + dynamic_(mkl_get_dynamic()), + tls_(mkl_set_num_threads_local(0)) { +#if defined(_M_X64) + mxcsr_ = _mm_getcsr(); + _mm_setcsr((_mm_getcsr() | 0x1F80U) & ~0x8040U); +#endif + static_cast(std::fesetround(FE_TONEAREST)); + mkl_set_dynamic(0); + } + ~RestoreTrialEnvironment() { + mkl_set_dynamic(dynamic_); + static_cast(mkl_set_num_threads_local(tls_)); + static_cast(std::fesetround(rounding_)); +#if defined(_M_X64) + _mm_setcsr(mxcsr_); +#endif + } + + private: + int rounding_; + int dynamic_; + int tls_; +#if defined(_M_X64) + unsigned int mxcsr_; +#endif +}; + +void ExpectSameBits(const double left, const double right) { + EXPECT_EQ(std::memcmp(&left, &right, sizeof(double)), 0); +} + +void ExpectSameOperators( + const fesa::hht_reduced_internal::ConstraintOperators& left, + const fesa::hht_reduced_internal::ConstraintOperators& right) { + ASSERT_EQ(left.rows.size(), right.rows.size()); + for (std::size_t row = 0U; row < left.rows.size(); ++row) { + const auto& a = left.rows[row]; + const auto& b = right.rows[row]; + ExpectSameBits(a.value, b.value); + ExpectSameBits(a.value_scale, b.value_scale); + ExpectSameBits(a.time_first, b.time_first); + ExpectSameBits(a.time_second, b.time_second); + for (const auto& pair : {std::make_pair(&a.gradient, &b.gradient), + std::make_pair(&a.time_gradient, &b.time_gradient), + std::make_pair(&a.third_directional_gradient, + &b.third_directional_gradient)}) { + ASSERT_EQ(pair.first->size(), pair.second->size()); + for (std::size_t i = 0U; i < pair.first->size(); ++i) { + EXPECT_EQ((*pair.first)[i].dof, (*pair.second)[i].dof); + ExpectSameBits((*pair.first)[i].value, (*pair.second)[i].value); + } + } + ASSERT_EQ(a.hessian.size(), b.hessian.size()); + for (std::size_t i = 0U; i < a.hessian.size(); ++i) { + EXPECT_EQ(a.hessian[i].row, b.hessian[i].row); + EXPECT_EQ(a.hessian[i].column, b.hessian[i].column); + ExpectSameBits(a.hessian[i].value, b.hessian[i].value); + } + } +} + struct Fixture { std::unique_ptr domain; fesa::DofManager dofs; @@ -394,6 +462,480 @@ TEST(HhtReducedChart, InvertibleGraphDoesNotCertifySelectorTransversality) { assembly, chart.Value(), solver).HasValue()); } +TEST(HhtReducedOperators, + CompleteTrialConstraintDataMatchesFreshAndOwnsCopies) { + RestoreTrialEnvironment restore; + const auto fixture = MakeFixture(false, true); + const auto shells = + fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs); + ASSERT_TRUE(shells.HasValue()); + const fesa::Vector zero(fixture.dofs.FullDofCount()); + auto q = zero; + auto v = zero; + auto force = zero; + auto rate = zero; + auto acceleration = zero; + for (std::size_t i = 0U; i < zero.Size(); ++i) { + q[i] = 0.001 * static_cast((i % 5U) + 1U); + v[i] = 0.002 * static_cast((i % 7U) + 1U); + if (i % 6U >= 3U) { + force[i] = 0.03; + rate[i] = -0.02; + acceleration[i] = 0.01; + } + } + auto assembled = Assemble(fixture, shells.Value(), q, v, zero, force, true); + ASSERT_TRUE(assembled.HasValue()); + ASSERT_TRUE(assembled.Value().trial_constraints.has_value()); + ASSERT_EQ(assembled.Value().trial_constraints->shells.size(), + shells.Value().size()); + const auto chart = fesa::hht_reduced_internal::BuildFixedChart( + fixture.dofs, assembled.Value().rotations); + ASSERT_TRUE(chart.HasValue()); + for (std::size_t e = 0U; e < shells.Value().size(); ++e) { + const auto& saved = assembled.Value().trial_constraints->shells[e]; + ASSERT_TRUE(saved.directional_internal); + fesa::NonlinearMitc4TrialDofs trial{}; + fesa::Vector direction{24U}; + for (std::size_t node = 0U; node < 4U; ++node) { + const auto first = 6U * node; + trial.displacement[node] = + fesa::Vector3{saved.q[first], saved.q[first + 1U], saved.q[first + 2U]}; + trial.rotation_vector[node] = fesa::Vector3{ + saved.q[first + 3U], saved.q[first + 4U], saved.q[first + 5U]}; + } + for (std::size_t i = 0U; i < 24U; ++i) direction[i] = saved.v[i]; + const auto response = fesa::EvaluateInternalResidualAndTangent( + shells.Value()[e].internal, trial, &direction); + ASSERT_TRUE(response.HasValue()); + for (std::size_t node = 0U; node < 4U; ++node) { + ExpectSameBits(saved.drilling_constraint[node], + response.Value().drilling_constraint[node]); + for (std::size_t i = 0U; i < 24U; ++i) { + ExpectSameBits(saved.drilling_constraint_gradient(node, i), + response.Value().drilling_constraint_gradient(node, i)); + ExpectSameBits( + saved.drilling_constraint_third_directional_gradient[node][i], + response.Value() + .drilling_constraint_third_directional_gradient[node][i]); + for (std::size_t j = 0U; j < 24U; ++j) + ExpectSameBits( + saved.drilling_constraint_hessian[node](i, j), + response.Value().drilling_constraint_hessian[node](i, j)); + } + } + } + const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) { + return fesa::hht_reduced_internal::EvaluateConstraintOperators( + *fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(), + rate, acceleration); + }; + auto fresh = assembled.Value(); + fresh.trial_constraints.reset(); + const auto reference = evaluate(fresh); + const auto reused = evaluate(assembled.Value()); + ASSERT_TRUE(reference.HasValue()); + ASSERT_TRUE(reused.HasValue()); + ExpectSameOperators(reused.Value(), reference.Value()); + auto copy = assembled.Value(); + copy.trial_constraints->shells[0U].drilling_constraint[0U] += 7.0; + // Test-only finite marker proves the selected route actually reads its owner. + const auto marker = evaluate(copy); + ASSERT_TRUE(marker.HasValue()); + EXPECT_NE(marker.Value().rows[0U].value, reused.Value().rows[0U].value); + EXPECT_NE( + copy.trial_constraints->shells[0U].drilling_constraint[0U], + assembled.Value().trial_constraints->shells[0U].drilling_constraint[0U]); + auto moved = std::move(assembled.Value()); + const auto moved_result = evaluate(moved); + ASSERT_TRUE(moved_result.HasValue()); + ExpectSameOperators(moved_result.Value(), reference.Value()); + fesa::DynamicAcceptedFrameState frame{}; + frame.generalized_coordinates = q; + frame.velocity = v; + frame.acceleration = zero; + frame.algorithmic_acceleration = zero; + frame.external_force = force; + const auto parameters = fesa::MakeHhtParameters(-0.05); + ASSERT_TRUE(parameters.HasValue()); + fesa::TbbParallelFor parallel; + const auto tbb = fesa::hht_internal::EvaluateAssembly( + *fixture.domain, fixture.dofs, shells.Value(), frame, zero, 0.1, + parameters.Value(), parallel, true); + ASSERT_TRUE(tbb.HasValue()); + const auto tbb_result = evaluate(tbb.Value()); + ASSERT_TRUE(tbb_result.HasValue()); + ExpectSameOperators(tbb_result.Value(), reference.Value()); +} + +TEST(HhtReducedOperators, + ChangedTrialConstraintContextFallsBackInOriginalPosition) { + RestoreTrialEnvironment restore; + const auto fixture = MakeFixture(false); + const auto shells = + fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs); + ASSERT_TRUE(shells.HasValue()); + const fesa::Vector zero(fixture.dofs.FullDofCount()); + const auto assembled = + Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); + ASSERT_TRUE(assembled.HasValue()); + ASSERT_TRUE(assembled.Value().trial_constraints.has_value()); + ASSERT_TRUE( + assembled.Value().trial_constraints->shells[0U].directional_internal); + const auto chart = fesa::hht_reduced_internal::BuildFixedChart( + fixture.dofs, assembled.Value().rotations); + ASSERT_TRUE(chart.HasValue()); + const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) { + return fesa::hht_reduced_internal::EvaluateConstraintOperators( + *fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(), + zero, zero); + }; + using Assembly = fesa::hht_internal::DynamicAssembly; + std::vector> changes{ + [](Assembly& a) { a.frame.generalized_coordinates[2U] = 0.001; }, + [](Assembly& a) { a.frame.velocity[3U] = 0.003; }, + [](Assembly& a) { a.frame.time = 0.1; }, + [](Assembly& a) { ++a.frame.frame_index; }, + [](Assembly& a) { ++a.frame.increment_index; }, + [](Assembly& a) { ++a.frame.iteration_count; }, + [](Assembly& a) { a.frame.is_initial = true; }, + [](Assembly& a) { a.trial_constraints->source_path = "different.inp"; }, + [](Assembly& a) { ++a.trial_constraints->shells[0U].element_index; }, + [](Assembly& a) { ++a.trial_constraints->shells[0U].scatter[0U]; }, + [](Assembly& a) { + a.trial_constraints->shells[0U].directional_internal = false; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U].node_source_ids[0U].instance_name += + "x"; + }, + [](Assembly& a) { + ++a.trial_constraints->shells[0U].node_source_ids[0U].source_label; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U].node_source_ids[0U].source_label_text += + "x"; + }, + [](Assembly& a) { + ++a.trial_constraints->shells[0U].configuration.source.line; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U].configuration.source.file = + "different.inp"; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U].configuration.identity += "x"; + }, + [](Assembly& a) { a.trial_constraints->shells[0U].q[0U] = -0.0; }, + [](Assembly& a) { a.trial_constraints->shells[0U].v[0U] = -0.0; }, + [](Assembly& a) { + a.trial_constraints->shells[0U].drilling_constraint_gradient = + fesa::Matrix{0U, 0U}; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U].drilling_constraint_hessian[2U] = + fesa::Matrix{0U, 0U}; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U] + .drilling_constraint_third_directional_gradient[1U] = + fesa::Vector{0U}; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U].drilling_constraint_hessian[0U]( + 0U, 0U) = std::numeric_limits::infinity(); + }, + [](Assembly& a) { + a.trial_constraints->shells[0U] + .evaluation_context.dense_policy.blas_max_threads = 2; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U] + .evaluation_context.dense_policy.dynamic = 1; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U] + .evaluation_context.dense_policy.dynamic = -1; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U].evaluation_context.mxcsr_control ^= + 0x8000U; + }, + [](Assembly& a) { + a.trial_constraints->shells[0U].evaluation_context.supported = false; + }}; + for (std::size_t i = 0U; i < 64U; ++i) + changes.push_back([i](Assembly& a) { + a.trial_constraints->shells[0U].configuration.values[i] += 0.125; + }); + for (const double invalid : {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}) + changes.push_back([invalid](Assembly& a) { + a.trial_constraints->shells[0U].configuration.values[60U] = invalid; + }); + for (std::size_t i = 0U; i < changes.size(); ++i) { + SCOPED_TRACE(i); + auto candidate = assembled.Value(); + candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0; + changes[i](candidate); + auto fresh = candidate; + fresh.trial_constraints.reset(); + const auto reference = evaluate(fresh); + const auto actual = evaluate(candidate); + ASSERT_TRUE(reference.HasValue()); + ASSERT_TRUE(actual.HasValue()); + ExpectSameOperators(actual.Value(), reference.Value()); + } + for (std::size_t kind = 0U; kind < 2U; ++kind) { + auto candidate = assembled.Value(); + if (kind == 0U) candidate.trial_constraints->shells.clear(); + if (kind == 1U) candidate.trial_constraints.reset(); + auto fresh = assembled.Value(); + fresh.trial_constraints.reset(); + const auto reference = evaluate(fresh); + const auto actual = evaluate(candidate); + ASSERT_TRUE(reference.HasValue()); + ASSERT_TRUE(actual.HasValue()); + ExpectSameOperators(actual.Value(), reference.Value()); + } + // Change the actual consumer environment, preserving the original fallback. + for (std::size_t mode = 0U; mode < 5U; ++mode) { + SCOPED_TRACE(mode); + auto candidate = assembled.Value(); + candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0; + auto fresh = candidate; + fresh.trial_constraints.reset(); +#if defined(_M_X64) + const auto csr = _mm_getcsr(); + if (mode == 0U) _mm_setcsr(csr ^ 0x8000U); + if (mode == 1U) _mm_setcsr(csr ^ 0x0040U); +#endif + const int rounding = std::fegetround(); + if (mode == 2U) static_cast(std::fesetround(FE_DOWNWARD)); + if (mode == 3U) static_cast(mkl_set_num_threads_local(2)); + if (mode == 4U) mkl_set_dynamic(1); + const auto reference = evaluate(fresh); + const auto actual = evaluate(candidate); + if (mode == 4U) mkl_set_dynamic(0); + if (mode == 3U) EXPECT_EQ(mkl_set_num_threads_local(0), 2); + static_cast(std::fesetround(rounding)); +#if defined(_M_X64) + _mm_setcsr(csr); +#endif + ASSERT_TRUE(reference.HasValue()); + ASSERT_TRUE(actual.HasValue()); + ExpectSameOperators(actual.Value(), reference.Value()); + } + auto failed_trial = assembled.Value(); + failed_trial.frame.generalized_coordinates[0U] = + std::numeric_limits::infinity(); + auto fresh_failure = failed_trial; + fresh_failure.trial_constraints.reset(); + const auto failed = evaluate(failed_trial); + const auto original = evaluate(fresh_failure); + ASSERT_FALSE(failed.HasValue()); + ASSERT_FALSE(original.HasValue()); + ASSERT_EQ(failed.GetStatus().Diagnostics().size(), + original.GetStatus().Diagnostics().size()); + EXPECT_EQ(failed.GetStatus().Diagnostics()[0U].code, + original.GetStatus().Diagnostics()[0U].code); + EXPECT_EQ(failed.GetStatus().Diagnostics()[0U].message, + original.GetStatus().Diagnostics()[0U].message); + const auto rerun = evaluate(assembled.Value()); + auto fresh = assembled.Value(); + fresh.trial_constraints.reset(); + const auto reference = evaluate(fresh); + ASSERT_TRUE(rerun.HasValue()); + ASSERT_TRUE(reference.HasValue()); + ExpectSameOperators(rerun.Value(), reference.Value()); +} + +TEST(HhtReducedOperators, TrialContextControlsAndInitialFalseFailureLifetime) { + RestoreTrialEnvironment restore; + const auto context = fesa::hht_internal::ReadTrialEvaluationContext(); + ASSERT_TRUE(fesa::hht_internal::ReusableTrialContext(context, context)); + for (const unsigned int bit : {0x0040U, 0x8000U, 0x2000U, 0x0080U}) { + auto changed = context; + changed.mxcsr_control ^= bit; + EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(context, changed)); + } + auto unmasked = context; + unmasked.mxcsr_control &= ~0x0080U; + EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(unmasked, unmasked)); + static_cast(mkl_set_num_threads_local(2)); + const auto positive = fesa::hht_internal::ReadTrialEvaluationContext(); + EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(positive, positive)); + EXPECT_EQ(mkl_set_num_threads_local(0), 2); + mkl_set_dynamic(1); + const auto dynamic = fesa::hht_internal::ReadTrialEvaluationContext(); + EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(dynamic, dynamic)); + mkl_set_dynamic(0); + const auto fixture = MakeFixture(false); + const auto shells = + fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs); + ASSERT_TRUE(shells.HasValue()); + const fesa::Vector zero(fixture.dofs.FullDofCount()); + const auto position = + Assemble(fixture, shells.Value(), zero, zero, zero, zero, false); + ASSERT_TRUE(position.HasValue()); + EXPECT_FALSE(position.Value().trial_constraints.has_value()); + mkl_set_dynamic(1); + const auto dynamic_trial = + Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); + mkl_set_dynamic(0); + ASSERT_TRUE(dynamic_trial.HasValue()); + ASSERT_TRUE(dynamic_trial.Value().trial_constraints.has_value()); + EXPECT_FALSE( + dynamic_trial.Value().trial_constraints->shells[0U].directional_internal); + static_cast(mkl_set_num_threads_local(2)); + const auto positive_trial = + Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); + EXPECT_EQ(mkl_set_num_threads_local(0), 2); + ASSERT_TRUE(positive_trial.HasValue()); + ASSERT_TRUE(positive_trial.Value().trial_constraints.has_value()); + EXPECT_FALSE(positive_trial.Value() + .trial_constraints->shells[0U] + .directional_internal); + fesa::DynamicAcceptedFrameState frame{}; + frame.is_initial = true; + for (auto* vector : + {&frame.generalized_coordinates, &frame.velocity, &frame.acceleration, + &frame.algorithmic_acceleration, &frame.external_force}) + *vector = zero; + fesa::SerialParallelFor parallel; + const auto parameters = fesa::MakeHhtParameters(-0.05); + ASSERT_TRUE(parameters.HasValue()); + const auto initial = fesa::hht_internal::EvaluateAssembly( + *fixture.domain, fixture.dofs, shells.Value(), frame, zero, 0.1, + parameters.Value(), parallel, true); + ASSERT_TRUE(initial.HasValue()); + EXPECT_FALSE(initial.Value().trial_constraints.has_value()); + auto invalid = zero; + invalid[0U] = std::numeric_limits::quiet_NaN(); + const auto failure = + Assemble(fixture, shells.Value(), invalid, zero, zero, zero, true); + EXPECT_FALSE(failure.HasValue()); + const auto current = + Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); + const auto next = + Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); + ASSERT_TRUE(current.HasValue()); + ASSERT_TRUE(next.HasValue()); + ASSERT_EQ(current.Value().trial_constraints->shells.size(), + shells.Value().size()); + ASSERT_EQ(next.Value().trial_constraints->shells.size(), + shells.Value().size()); + EXPECT_NE(¤t.Value().trial_constraints->shells[0U], + &next.Value().trial_constraints->shells[0U]); +} + +TEST(HhtReducedOperators, NonfiniteMatchingTrialKeysPreserveOriginalFailure) { + RestoreTrialEnvironment restore; + const auto fixture = MakeFixture(false); + const auto shells = + fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs); + ASSERT_TRUE(shells.HasValue()); + const fesa::Vector zero(fixture.dofs.FullDofCount()); + const auto assembled = + Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); + ASSERT_TRUE(assembled.HasValue()); + ASSERT_TRUE(assembled.Value().trial_constraints.has_value()); + ASSERT_TRUE( + assembled.Value().trial_constraints->shells[0U].directional_internal); + const auto chart = fesa::hht_reduced_internal::BuildFixedChart( + fixture.dofs, assembled.Value().rotations); + ASSERT_TRUE(chart.HasValue()); + const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) { + return fesa::hht_reduced_internal::EvaluateConstraintOperators( + *fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(), + zero, zero); + }; + const std::array invalid{std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + for (std::size_t channel = 0U; channel < 2U; ++channel) + for (std::size_t i = 0U; i < invalid.size(); ++i) { + SCOPED_TRACE(channel); + SCOPED_TRACE(i); + auto candidate = assembled.Value(); + auto& saved = candidate.trial_constraints->shells[0U]; + if (channel == 0U) { + candidate.frame.generalized_coordinates[0U] = invalid[i]; + saved.q[0U] = invalid[i]; + } else { + candidate.frame.velocity[0U] = invalid[i]; + saved.v[0U] = invalid[i]; + } + auto fresh = candidate; + fresh.trial_constraints.reset(); + const auto original = evaluate(fresh); + const auto actual = evaluate(candidate); + ASSERT_FALSE(original.HasValue()); + ASSERT_FALSE(actual.HasValue()); + EXPECT_EQ(actual.GetStatus().Category(), original.GetStatus().Category()); + const auto& a = actual.GetStatus().Diagnostics(); + const auto& b = original.GetStatus().Diagnostics(); + ASSERT_EQ(a.size(), b.size()); + for (std::size_t j = 0U; j < a.size(); ++j) { + EXPECT_EQ(a[j].severity, b[j].severity); + EXPECT_EQ(a[j].code, b[j].code); + EXPECT_EQ(a[j].location.file, b[j].location.file); + EXPECT_EQ(a[j].location.line, b[j].location.line); + EXPECT_EQ(a[j].keyword, b[j].keyword); + EXPECT_EQ(a[j].entity_identity, b[j].entity_identity); + EXPECT_EQ(a[j].message, b[j].message); + } + } +} + +TEST(HhtReducedOperators, NonfiniteMatchingTimeKeyMissPreservesFreshSuccess) { + RestoreTrialEnvironment restore; + const auto fixture = MakeFixture(false); + const auto shells = + fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs); + ASSERT_TRUE(shells.HasValue()); + const fesa::Vector zero(fixture.dofs.FullDofCount()); + const auto assembled = + Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); + ASSERT_TRUE(assembled.HasValue()); + ASSERT_TRUE(assembled.Value().trial_constraints.has_value()); + const auto chart = fesa::hht_reduced_internal::BuildFixedChart( + fixture.dofs, assembled.Value().rotations); + ASSERT_TRUE(chart.HasValue()); + const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) { + return fesa::hht_reduced_internal::EvaluateConstraintOperators( + *fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(), + zero, zero); + }; + const auto parameters = fesa::MakeHhtParameters(-0.05); + ASSERT_TRUE(parameters.HasValue()); + fesa::SerialParallelFor parallel; + for (const double bad_time : {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}) { + auto candidate = assembled.Value(); + candidate.frame.time = bad_time; + candidate.trial_constraints->time = bad_time; + candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0; + auto fresh = candidate; + fresh.trial_constraints.reset(); + const auto reference = evaluate(fresh); + const auto actual = evaluate(candidate); + ASSERT_TRUE(reference.HasValue()); + ASSERT_TRUE(actual.HasValue()); + ExpectSameOperators(actual.Value(), reference.Value()); + const auto producer = fesa::hht_internal::EvaluateAssembly( + *fixture.domain, fixture.dofs, shells.Value(), candidate.frame, zero, + 0.1, parameters.Value(), parallel, true); + ASSERT_TRUE(producer.HasValue()); + EXPECT_FALSE(producer.Value().trial_constraints.has_value()); + const auto producer_result = evaluate(producer.Value()); + ASSERT_TRUE(producer_result.HasValue()); + ExpectSameOperators(producer_result.Value(), reference.Value()); + } +} + TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) { const auto fixture = MakeFixture(false); const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, diff --git a/tests/unit/math/dense_blas_internal_test.cpp b/tests/unit/math/dense_blas_internal_test.cpp index d3be2a4..7ddfb66 100644 --- a/tests/unit/math/dense_blas_internal_test.cpp +++ b/tests/unit/math/dense_blas_internal_test.cpp @@ -11,6 +11,39 @@ namespace fesa::dense_blas_internal { namespace { +TEST(DenseBlasInternal, PolicyQueryPreservesDefaultPositiveAndNestedTls) { + const int previous = mkl_set_num_threads_local(0); + struct Restore { + int previous; + ~Restore() { static_cast(mkl_set_num_threads_local(previous)); } + } restore{previous}; + const int global = mkl_get_max_threads(); + const int domain = mkl_domain_get_max_threads(MKL_DOMAIN_BLAS); + const int dynamic = mkl_get_dynamic(); + for (int repeat = 0; repeat < 2; ++repeat) { + const auto policy = ReadScopedLocalDensePolicy(); + EXPECT_EQ(policy.blas_max_threads, 1); + EXPECT_EQ(policy.dynamic, dynamic); + EXPECT_EQ(mkl_set_num_threads_local(0), 0); + EXPECT_EQ(mkl_get_max_threads(), global); + EXPECT_EQ(mkl_domain_get_max_threads(MKL_DOMAIN_BLAS), domain); + EXPECT_EQ(mkl_get_dynamic(), dynamic); + } + static_cast(mkl_set_num_threads_local(2)); + EXPECT_EQ(ReadScopedLocalDensePolicy().blas_max_threads, 2); + EXPECT_EQ(mkl_set_num_threads_local(2), 2); + static_cast(mkl_set_num_threads_local(0)); + try { + const ScopedLocalDenseThreads outer{}; + EXPECT_EQ(ReadScopedLocalDensePolicy().blas_max_threads, 1); + EXPECT_EQ(mkl_set_num_threads_local(1), 1); + throw std::runtime_error{"policy-query-unwind"}; + } catch (const std::runtime_error&) { + } + EXPECT_EQ(mkl_set_num_threads_local(0), 0); + EXPECT_EQ(mkl_get_dynamic(), dynamic); +} + TEST(DenseBlasInternal, OuterProductUsesRectangularStorageAndPreservesRejection) { const std::array left{2.0, -3.0};