From 12d51480ded95d070f7db45d7ca6e8b0a2d68c77 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 18 Oct 2026 02:10:51 +0900 Subject: [PATCH] Integrate Refactor solver sources and dual MSVC CMake builds --- .gitignore | 4 + CMakeLists.txt | 10 +- CMakePresets.json | 28 +- FESA.sln | 48 + .../merge-manifest.json | 3865 +++++++++++++++- include/fesa/analysis/analysis_procedure.h | 23 + include/fesa/analysis/analysis_state.h | 3 +- .../fesa/analysis/dynamic_execution_result.h | 19 + include/fesa/analysis/eigen_analysis_state.h | 2 +- include/fesa/analysis/hht_dynamic_procedure.h | 6 +- .../analysis/linear_frequency_procedure.h | 10 + .../analysis/linear_static_execution_result.h | 19 + .../fesa/analysis/linear_static_procedure.h | 5 +- .../nonlinear_static_execution_result.h | 20 + .../analysis/nonlinear_static_procedure.h | 5 +- include/fesa/analysis/prepared_analysis.h | 51 +- .../fesa/analysis/prepared_analysis_outcome.h | 26 + .../analysis/solid_dynamic_execution_result.h | 17 + .../fesa/analysis/solid_frequency_procedure.h | 10 + include/fesa/analysis/solid_frequency_state.h | 2 +- .../analysis/solid_hht_dynamic_procedure.h | 8 +- .../analysis/solid_static_execution_result.h | 19 + .../fesa/analysis/solid_static_procedure.h | 8 +- include/fesa/analysis/solid_static_state.h | 2 +- .../fesa/analysis/thermal_analysis_state.h | 6 +- .../fesa/analysis/thermal_execution_result.h | 19 + .../fesa/analysis/transient_heat_procedure.h | 11 +- include/fesa/assembly/load_assembler.h | 3 +- include/fesa/assembly/thermal_assembler.h | 7 + .../constraints/essential_constraint_policy.h | 10 + .../constraints/prescribed_displacement.h | 2 +- include/fesa/core/ascii.h | 6 + include/fesa/core/source_identity.h | 8 + include/fesa/core/status.h | 4 +- include/fesa/elements/element.h | 25 +- include/fesa/elements/element_capabilities.h | 200 + include/fesa/elements/element_dof_layout.h | 38 + include/fesa/elements/element_factory.h | 9 + include/fesa/elements/euler_beam_3d.h | 35 +- .../fesa/elements/euler_beam_3d_definition.h | 48 + include/fesa/elements/finite_rotation.h | 10 +- include/fesa/elements/mitc4_shell.h | 50 +- .../fesa/elements/mitc4_shell_definition.h | 60 + include/fesa/elements/thermal_dof_component.h | 20 + .../fesa/elements/thermal_element_layout.h | 3 +- include/fesa/fem/dof_manager.h | 11 +- include/fesa/math/matrix.h | 20 + include/fesa/math/sparse_matrix.h | 3 +- include/fesa/math/sparse_pattern.h | 18 + include/fesa/model/domain.h | 2 + include/fesa/model/model_types.h | 17 +- include/fesa/model/source_target_resolver.h | 8 + .../fesa/results/prepared_results_writer.h | 39 + include/fesa/solvers/eigen/eigen_solver.h | 99 + .../solvers/eigen/mkl_feast_eigen_solver.h | 67 +- msvc/AGENTS.md | 17 + msvc/Fesa.Common.props | 75 + msvc/Fesa.Runtime.targets | 23 + msvc/fesa_cli.vcxproj | 18 + msvc/fesa_integration_tests.vcxproj | 30 + msvc/fesa_reference_tests.vcxproj | 35 + msvc/fesa_solver.vcxproj | 118 + msvc/fesa_unit_tests.vcxproj | 108 + msvc/gtest.vcxproj | 19 + src/fesa/CMakeLists.txt | 3 + src/fesa/analysis/analysis_procedures.h | 170 + src/fesa/analysis/analysis_state.cpp | 33 +- src/fesa/analysis/dynamic_analysis_state.cpp | 65 +- src/fesa/analysis/eigen_reduction.cpp | 41 +- src/fesa/analysis/equilibrium_algorithm.h | 86 + src/fesa/analysis/hht_dynamic_algebra.cpp | 96 +- src/fesa/analysis/hht_dynamic_assembly.cpp | 39 +- src/fesa/analysis/hht_dynamic_internal.h | 14 +- src/fesa/analysis/hht_reduced_integrator.h | 74 + src/fesa/analysis/hht_reduced_operators.cpp | 78 +- src/fesa/analysis/hht_reduced_procedure.cpp | 864 ++-- src/fesa/analysis/hht_reduced_procedure.h | 14 +- .../analysis/linear_frequency_procedure.cpp | 16 +- src/fesa/analysis/linear_static_procedure.cpp | 2 +- .../analysis/nonlinear_analysis_state.cpp | 67 +- .../analysis/nonlinear_static_control.cpp | 59 +- src/fesa/analysis/nonlinear_static_core.h | 16 + .../analysis/nonlinear_static_procedure.cpp | 520 ++- src/fesa/analysis/prepared_analysis.cpp | 434 +- src/fesa/analysis/shell_state_inventory.h | 25 + .../analysis/solid_dynamic_analysis_state.cpp | 17 +- .../analysis/solid_frequency_procedure.cpp | 20 +- src/fesa/analysis/solid_hht_core.h | 20 + .../analysis/solid_hht_dynamic_procedure.cpp | 368 +- src/fesa/analysis/solid_hht_integrator.h | 75 + src/fesa/analysis/solid_static_core.h | 27 + src/fesa/analysis/solid_static_procedure.cpp | 228 +- src/fesa/analysis/solid_static_state.cpp | 9 +- src/fesa/analysis/thermal_analysis_state.cpp | 11 +- .../analysis/thermal_transient_algebra.cpp | 44 +- src/fesa/analysis/thermal_transient_algebra.h | 51 + .../analysis/transient_heat_procedure.cpp | 1 + src/fesa/app/fesa_application.cpp | 28 +- src/fesa/assembly/load_assembler.cpp | 119 +- .../assembly/ordered_contribution_internal.h | 58 + src/fesa/assembly/sparse_assembler.cpp | 24 +- .../assembly/surface_pressure_assembler.cpp | 16 +- src/fesa/assembly/thermal_assembler.cpp | 37 +- .../essential_constraint_policy.cpp | 125 + src/fesa/core/ascii.cpp | 12 +- src/fesa/elements/element_capabilities.cpp | 115 + src/fesa/elements/element_factory.cpp | 12 + src/fesa/elements/finite_rotation.cpp | 71 +- src/fesa/elements/hex8_element.cpp | 39 +- src/fesa/elements/hex8_geometry.cpp | 13 - src/fesa/elements/hex8_geometry.h | 2 - src/fesa/elements/hex8_high_low.cpp | 5 +- src/fesa/elements/hex8_pair_arithmetic.h | 40 - src/fesa/elements/hex8_pair_simd.h | 6 +- src/fesa/elements/hex8_pair_simd_avx2.cpp | 12 +- src/fesa/elements/hex8_pair_simd_dispatch.cpp | 26 +- src/fesa/elements/mitc4_shell.cpp | 161 +- src/fesa/elements/nonlinear_mitc4_inertia.cpp | 217 +- src/fesa/elements/nonlinear_mitc4_shell.cpp | 314 +- src/fesa/elements/shell_kernel_values.h | 79 + src/fesa/elements/thermal_shell_geometry.cpp | 83 +- src/fesa/elements/thermal_shell_geometry.h | 2 +- src/fesa/fem/dof_manager.cpp | 13 +- src/fesa/io/abaqus/domain_mapping_context.cpp | 3939 ++--------------- src/fesa/io/abaqus/input_reader.cpp | 26 +- src/fesa/io/abaqus/mapping_diagnostics.h | 105 + .../io/abaqus/material_property_candidates.h | 185 + .../io/abaqus/material_property_mapper.cpp | 918 ++++ .../io/abaqus/step_definition_candidates.h | 196 + src/fesa/io/abaqus/step_definition_mapper.cpp | 1670 +++++++ .../io/abaqus/surface_definition_candidates.h | 87 + .../io/abaqus/surface_definition_mapper.cpp | 354 ++ src/fesa/io/abaqus/topology_candidates.h | 133 + src/fesa/io/abaqus/topology_mapper.cpp | 405 ++ .../io/hdf5/dynamic_hdf5_results_writer.cpp | 90 +- .../io/hdf5/eigen_hdf5_results_writer.cpp | 28 +- src/fesa/io/hdf5/eigen_hdf5_self_check.cpp | 8 +- src/fesa/io/hdf5/hdf5_model_writer.cpp | 8 +- src/fesa/io/hdf5/hdf5_primitives.cpp | 89 +- src/fesa/io/hdf5/hdf5_primitives.h | 43 +- src/fesa/io/hdf5/hdf5_result_writer.cpp | 12 +- src/fesa/io/hdf5/hdf5_self_check.cpp | 11 +- .../io/hdf5/nonlinear_hdf5_results_writer.cpp | 127 +- .../io/hdf5/solid_hdf5_results_writer.cpp | 19 +- src/fesa/io/hdf5/thermal_hdf5_self_check.cpp | 9 +- src/fesa/loads/shell_surface_pressure.cpp | 13 +- src/fesa/math/compensated_sum.h | 30 + src/fesa/math/dense_blas_internal.cpp | 335 +- src/fesa/math/dense_blas_internal.h | 112 +- src/fesa/math/finite_values.h | 40 + src/fesa/math/high_low_arithmetic.h | 19 +- src/fesa/math/jet_arithmetic.h | 38 + src/fesa/math/jet_linear_solve.h | 85 + src/fesa/math/matrix.cpp | 72 +- src/fesa/math/sparse_matrix.cpp | 1 - src/fesa/math/vector.cpp | 39 +- src/fesa/math/vector3_operations.h | 57 + src/fesa/model/shell_geometry.cpp | 56 +- src/fesa/model/source_target_resolver.cpp | 22 +- src/fesa/results/prepared_results_writer.cpp | 83 + src/fesa/results/recovery_candidate.cpp | 11 +- src/fesa/results/recovery_candidate.h | 6 +- src/fesa/results/result_recovery.cpp | 17 +- src/fesa/results/shell_result_recovery.cpp | 3 - src/fesa/results/shell_result_recovery.h | 3 + .../solvers/eigen/mkl_feast_eigen_solver.cpp | 23 +- .../solvers/linear/mkl_pardiso_solver.cpp | 334 +- .../linear/mkl_pardiso_tangent_solver.cpp | 284 +- src/fesa/solvers/linear/pardiso_session.cpp | 384 ++ src/fesa/solvers/linear/pardiso_session.h | 60 + .../linear/pardiso_session_test_seam.h | 41 + tests/CMakeLists.txt | 1 + .../nonlinear_static_procedure_test.cpp | 19 +- .../analysis/thermal_physics_test.cpp | 2 + .../app/thermal_application_test.cpp | 2 + .../reference/eigen_reference_cases_test.cpp | 11 +- .../nonlinear_static_reference_comparison.cpp | 19 +- .../thermal_reference_cases_test.cpp | 9 +- tests/test_compare_hex8.py | 856 ++++ tests/test_compare_hht_dynamic.py | 351 ++ tests/unit/analysis/analysis_state_test.cpp | 3 + tests/unit/analysis/analysis_test.cpp | 563 +++ tests/unit/analysis/eigen_reduction_test.cpp | 100 +- .../unit/analysis/hht_reduced_shell_test.cpp | 596 --- .../unit/analysis/hht_shell_physics_test.cpp | 12 +- .../solid_hht_dynamic_procedure_test.cpp | 2 + .../analysis/solid_static_procedure_test.cpp | 117 + .../analysis/thermal_analysis_state_test.cpp | 2 + .../transient_heat_procedure_test.cpp | 2 + tests/unit/assembly/load_assembler_test.cpp | 44 - tests/unit/assembly/sparse_assembler_test.cpp | 65 +- .../essential_constraints_test.cpp | 68 + tests/unit/core/ascii_test.cpp | 15 + tests/unit/core/source_identity_test.cpp | 8 + tests/unit/core/status_test.cpp | 61 + .../elements/element_capabilities_test.cpp | 413 ++ tests/unit/elements/finite_rotation_test.cpp | 37 +- tests/unit/elements/hex8_element_test.cpp | 44 +- tests/unit/elements/hex8_high_low_test.cpp | 933 ---- tests/unit/elements/mitc4_shell_test.cpp | 13 + .../elements/nonlinear_mitc4_inertia_test.cpp | 18 +- tests/unit/fem/dof_manager_test.cpp | 3 + .../hdf5/solid_hdf5_results_writer_test.cpp | 3 + tests/unit/math/dense_blas_internal_test.cpp | 597 ++- tests/unit/math/high_low_arithmetic_test.cpp | 177 +- tests/unit/math/matrix_test.cpp | 71 +- tests/unit/math/sparse_matrix_test.cpp | 102 - tests/unit/math/vector3_test.cpp | 14 + tests/unit/math/vector_test.cpp | 27 + tests/unit/model/shell_geometry_test.cpp | 11 + .../model/source_target_resolver_test.cpp | 38 + .../model/thermal_shell_geometry_test.cpp | 66 + tests/unit/results/result_records_test.cpp | 3 + .../result_recovery_components_test.cpp | 36 - tests/unit/results/results_writer_test.cpp | 191 + .../solvers/linear/linear_solver_test.cpp | 283 ++ .../linear/mkl_pardiso_solver_test.cpp | 64 - .../solvers/linear/tangent_solver_test.cpp | 102 - 218 files changed, 17842 insertions(+), 9689 deletions(-) create mode 100644 FESA.sln create mode 100644 include/fesa/analysis/analysis_procedure.h create mode 100644 include/fesa/analysis/dynamic_execution_result.h create mode 100644 include/fesa/analysis/linear_static_execution_result.h create mode 100644 include/fesa/analysis/nonlinear_static_execution_result.h create mode 100644 include/fesa/analysis/prepared_analysis_outcome.h create mode 100644 include/fesa/analysis/solid_dynamic_execution_result.h create mode 100644 include/fesa/analysis/solid_static_execution_result.h create mode 100644 include/fesa/analysis/thermal_execution_result.h create mode 100644 include/fesa/elements/element_capabilities.h create mode 100644 include/fesa/elements/element_dof_layout.h create mode 100644 include/fesa/elements/euler_beam_3d_definition.h create mode 100644 include/fesa/elements/mitc4_shell_definition.h create mode 100644 include/fesa/elements/thermal_dof_component.h create mode 100644 include/fesa/math/sparse_pattern.h create mode 100644 include/fesa/results/prepared_results_writer.h create mode 100644 include/fesa/solvers/eigen/eigen_solver.h create mode 100644 msvc/AGENTS.md create mode 100644 msvc/Fesa.Common.props create mode 100644 msvc/Fesa.Runtime.targets create mode 100644 msvc/fesa_cli.vcxproj create mode 100644 msvc/fesa_integration_tests.vcxproj create mode 100644 msvc/fesa_reference_tests.vcxproj create mode 100644 msvc/fesa_solver.vcxproj create mode 100644 msvc/fesa_unit_tests.vcxproj create mode 100644 msvc/gtest.vcxproj create mode 100644 src/fesa/analysis/analysis_procedures.h create mode 100644 src/fesa/analysis/equilibrium_algorithm.h create mode 100644 src/fesa/analysis/hht_reduced_integrator.h create mode 100644 src/fesa/analysis/nonlinear_static_core.h create mode 100644 src/fesa/analysis/shell_state_inventory.h create mode 100644 src/fesa/analysis/solid_hht_core.h create mode 100644 src/fesa/analysis/solid_hht_integrator.h create mode 100644 src/fesa/analysis/solid_static_core.h create mode 100644 src/fesa/assembly/ordered_contribution_internal.h create mode 100644 src/fesa/elements/element_capabilities.cpp delete mode 100644 src/fesa/elements/hex8_pair_arithmetic.h create mode 100644 src/fesa/elements/shell_kernel_values.h create mode 100644 src/fesa/io/abaqus/mapping_diagnostics.h create mode 100644 src/fesa/io/abaqus/material_property_candidates.h create mode 100644 src/fesa/io/abaqus/step_definition_candidates.h create mode 100644 src/fesa/io/abaqus/surface_definition_candidates.h create mode 100644 src/fesa/io/abaqus/topology_candidates.h create mode 100644 src/fesa/math/compensated_sum.h create mode 100644 src/fesa/math/finite_values.h create mode 100644 src/fesa/math/jet_arithmetic.h create mode 100644 src/fesa/math/jet_linear_solve.h create mode 100644 src/fesa/math/vector3_operations.h create mode 100644 src/fesa/results/prepared_results_writer.cpp create mode 100644 src/fesa/solvers/linear/pardiso_session.cpp create mode 100644 src/fesa/solvers/linear/pardiso_session.h create mode 100644 src/fesa/solvers/linear/pardiso_session_test_seam.h create mode 100644 tests/test_compare_hex8.py create mode 100644 tests/test_compare_hht_dynamic.py create mode 100644 tests/unit/elements/element_capabilities_test.cpp diff --git a/.gitignore b/.gitignore index ba1bef0..6f7db5e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,7 @@ phases/**/step*-output.json # local document backups and initialization state .dryforge/ + +# direct MSVC and CMake local build state +/.build/ +/msvc/Fesa.Local.props diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f23b0c..5cd78db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,14 @@ cmake_minimum_required(VERSION 3.25) - project(FESA VERSION 0.1.0 LANGUAGES CXX) - set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) - +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +# Pin the original backend ABI/link/thread choices rather than package defaults. +set(MKL_INTERFACE ilp64) +set(MKL_LINK dynamic) +set(MKL_THREADING intel_thread) include(cmake/FesaDependencies.cmake) - find_package(Doxygen QUIET) if(Doxygen_FOUND) add_custom_target(fesa_docs @@ -19,6 +20,5 @@ if(Doxygen_FOUND) endif() enable_testing() - add_subdirectory(src/fesa) add_subdirectory(tests) diff --git a/CMakePresets.json b/CMakePresets.json index 7074f53..38b16a4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -3,11 +3,11 @@ "configurePresets": [ { "name": "windows-msvc-ninja-debug", - "displayName": "Windows MSVC x64 Debug (Ninja)", "generator": "Ninja", - "binaryDir": "${sourceDir}/build/windows-msvc-ninja-debug", + "binaryDir": "${sourceDir}/.build/cmake-debug", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", "FESA_GTEST_SOURCE_DIR": "$env{FESA_GTEST_SOURCE_DIR}", "MKL_DIR": "$env{MKL_DIR}", "TBB_DIR": "$env{TBB_DIR}", @@ -16,11 +16,15 @@ }, { "name": "windows-msvc-ninja-release", - "displayName": "Windows MSVC x64 Release with symbols (Ninja)", - "inherits": "windows-msvc-ninja-debug", - "binaryDir": "${sourceDir}/build/windows-msvc-ninja-release", + "generator": "Ninja", + "binaryDir": "${sourceDir}/.build/cmake-release", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "FESA_GTEST_SOURCE_DIR": "$env{FESA_GTEST_SOURCE_DIR}", + "MKL_DIR": "$env{MKL_DIR}", + "TBB_DIR": "$env{TBB_DIR}", + "HDF5_DIR": "$env{HDF5_DIR}" } } ], @@ -37,7 +41,17 @@ "testPresets": [ { "name": "windows-msvc-ninja-debug", - "configurePreset": "windows-msvc-ninja-debug" + "configurePreset": "windows-msvc-ninja-debug", + "output": { + "outputOnFailure": true + } + }, + { + "name": "windows-msvc-ninja-release", + "configurePreset": "windows-msvc-ninja-release", + "output": { + "outputOnFailure": true + } } ] } diff --git a/FESA.sln b/FESA.sln new file mode 100644 index 0000000..64295f3 --- /dev/null +++ b/FESA.sln @@ -0,0 +1,48 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_solver", "msvc\fesa_solver.vcxproj", "{3AEC39D9-4641-5161-9107-265EB50CA882}" +EndProject +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_cli", "msvc\fesa_cli.vcxproj", "{6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}" +EndProject +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_unit_tests", "msvc\fesa_unit_tests.vcxproj", "{42558F77-73F2-5CA7-8E3C-BB0E1646CD45}" +EndProject +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_integration_tests", "msvc\fesa_integration_tests.vcxproj", "{7770C26B-DBFE-505D-8788-8B7EFD9A5A70}" +EndProject +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "fesa_reference_tests", "msvc\fesa_reference_tests.vcxproj", "{330D2DFB-E57D-517D-A795-F1EA620B69F0}" +EndProject +Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "gtest", "msvc\gtest.vcxproj", "{03194BA4-3F3B-5399-BCAF-FB4FA47300CE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3AEC39D9-4641-5161-9107-265EB50CA882}.Debug|x64.ActiveCfg = Debug|x64 + {3AEC39D9-4641-5161-9107-265EB50CA882}.Debug|x64.Build.0 = Debug|x64 + {3AEC39D9-4641-5161-9107-265EB50CA882}.Release|x64.ActiveCfg = Release|x64 + {3AEC39D9-4641-5161-9107-265EB50CA882}.Release|x64.Build.0 = Release|x64 + {6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}.Debug|x64.ActiveCfg = Debug|x64 + {6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}.Debug|x64.Build.0 = Debug|x64 + {6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}.Release|x64.ActiveCfg = Release|x64 + {6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}.Release|x64.Build.0 = Release|x64 + {42558F77-73F2-5CA7-8E3C-BB0E1646CD45}.Debug|x64.ActiveCfg = Debug|x64 + {42558F77-73F2-5CA7-8E3C-BB0E1646CD45}.Debug|x64.Build.0 = Debug|x64 + {42558F77-73F2-5CA7-8E3C-BB0E1646CD45}.Release|x64.ActiveCfg = Release|x64 + {42558F77-73F2-5CA7-8E3C-BB0E1646CD45}.Release|x64.Build.0 = Release|x64 + {7770C26B-DBFE-505D-8788-8B7EFD9A5A70}.Debug|x64.ActiveCfg = Debug|x64 + {7770C26B-DBFE-505D-8788-8B7EFD9A5A70}.Debug|x64.Build.0 = Debug|x64 + {7770C26B-DBFE-505D-8788-8B7EFD9A5A70}.Release|x64.ActiveCfg = Release|x64 + {7770C26B-DBFE-505D-8788-8B7EFD9A5A70}.Release|x64.Build.0 = Release|x64 + {330D2DFB-E57D-517D-A795-F1EA620B69F0}.Debug|x64.ActiveCfg = Debug|x64 + {330D2DFB-E57D-517D-A795-F1EA620B69F0}.Debug|x64.Build.0 = Debug|x64 + {330D2DFB-E57D-517D-A795-F1EA620B69F0}.Release|x64.ActiveCfg = Release|x64 + {330D2DFB-E57D-517D-A795-F1EA620B69F0}.Release|x64.Build.0 = Release|x64 + {03194BA4-3F3B-5399-BCAF-FB4FA47300CE}.Debug|x64.ActiveCfg = Debug|x64 + {03194BA4-3F3B-5399-BCAF-FB4FA47300CE}.Debug|x64.Build.0 = Debug|x64 + {03194BA4-3F3B-5399-BCAF-FB4FA47300CE}.Release|x64.ActiveCfg = Release|x64 + {03194BA4-3F3B-5399-BCAF-FB4FA47300CE}.Release|x64.Build.0 = Release|x64 + EndGlobalSection +EndGlobal diff --git a/docs/cpp-object-oriented-modular-refactoring/merge-manifest.json b/docs/cpp-object-oriented-modular-refactoring/merge-manifest.json index 0d871fa..73cf85e 100644 --- a/docs/cpp-object-oriented-modular-refactoring/merge-manifest.json +++ b/docs/cpp-object-oriented-modular-refactoring/merge-manifest.json @@ -3140,6 +3140,15 @@ "exists": true, "size_bytes": 882, "sha256": "503e0165646fe2abe75b4ad38a8e96efd3c8b6b78202825676433141a903e92e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "503e0165646fe2abe75b4ad38a8e96efd3c8b6b78202825676433141a903e92e", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3182,6 +3191,15 @@ "exists": true, "size_bytes": 3851, "sha256": "efe503b98fc6d534b32ecc53bd3323708908b32ba3d5432fec17ce413a601eda" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "efe503b98fc6d534b32ecc53bd3323708908b32ba3d5432fec17ce413a601eda", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3202,7 +3220,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "b3e78f586752d5d43034c5a5202ec5fd862e3101837084162a572237dd26c148", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/analysis/analysis_state.h", @@ -3244,6 +3274,18 @@ "exists": true, "size_bytes": 4484, "sha256": "0a7be4c8d0bd0f5f95f6bf938709a1fb6317b44016417510cc5a56ee96e2d32e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "d5b19bb4434d79fa3e648e0de3cd0e0bc4eec37be7cf9f8d839ff876dc2d1716", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -3286,6 +3328,15 @@ "exists": true, "size_bytes": 4799, "sha256": "89065f2c9a029624edb22a407cc2b61c9cd2d735c7563d3c9b71eb2f1870ca00" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "89065f2c9a029624edb22a407cc2b61c9cd2d735c7563d3c9b71eb2f1870ca00", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3306,7 +3357,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ac52e12215ba15794855638e4e3702b504418d88ec390970cd3935e3c8baf9c2", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/analysis/eigen_analysis_state.h", @@ -3348,6 +3411,18 @@ "exists": true, "size_bytes": 2282, "sha256": "885d90420bedec17175e320e4f7c7f8fc1e230db71991d195a1782577a853645" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "13ad5f4d9fc4a65c6c69f26a9b1f7041470a81c4b33d7fe082878d043f63ebe4", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -3390,6 +3465,18 @@ "exists": true, "size_bytes": 1043, "sha256": "81ac8359580ca0adfdf73fa588aa7c654dfdf6dc257934982841673ae3de9163" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ef2fef93b46aabd47c9511b8afd9e6f82779036b484e0090da60d925a0599dec", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -3432,6 +3519,15 @@ "exists": true, "size_bytes": 1822, "sha256": "ccd94ef6314854dce1de682dd7df0488638f1e98b1a1f0e6c8824a220b138809" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ccd94ef6314854dce1de682dd7df0488638f1e98b1a1f0e6c8824a220b138809", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3474,6 +3570,18 @@ "exists": true, "size_bytes": 623, "sha256": "e291b9bbd6e863feeafea4c96ab5863e0b97549ebe11825ae2bd323db4360eeb" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "db7a1a2a1e10dd1a11de7ad0c613670f957f9609c02b83d00334eaa45ec2627a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -3516,6 +3624,15 @@ "exists": true, "size_bytes": 1138, "sha256": "5177f62a87e96e2591b4f5fe21cc3807be0606ce512a28b99a3d462bc195477a" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "5177f62a87e96e2591b4f5fe21cc3807be0606ce512a28b99a3d462bc195477a", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3536,7 +3653,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "206677b67093eff13d7425d9c87733d139d87e0b9092e0e3eecaced54292c905", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/analysis/linear_static_procedure.h", @@ -3578,6 +3707,18 @@ "exists": true, "size_bytes": 1089, "sha256": "93ab210acb8fd880f5394b281311865e0c1f995d3ec36f739394858c8a62aacf" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "cb6db8132fbcf96e737ca327511b60d63aa7b2bcd849a48fc43ac2831012985c", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -3620,6 +3761,15 @@ "exists": true, "size_bytes": 4505, "sha256": "e6a4224a8670218317b60836b4bdb9a6a22462309850b8b978e265e574e04174" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e6a4224a8670218317b60836b4bdb9a6a22462309850b8b978e265e574e04174", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3662,6 +3812,15 @@ "exists": true, "size_bytes": 4408, "sha256": "e1231e7db299ea2c24d0da8e6553d2990d7905cd28c3a03d4126116fa4ba8ae1" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e1231e7db299ea2c24d0da8e6553d2990d7905cd28c3a03d4126116fa4ba8ae1", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3704,6 +3863,15 @@ "exists": true, "size_bytes": 4952, "sha256": "a7b6368fd2b87fedeb4cf39566848f8136433c7f25d9ec15fc0cdc2043647c15" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a7b6368fd2b87fedeb4cf39566848f8136433c7f25d9ec15fc0cdc2043647c15", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3724,7 +3892,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "5c870e6a9941e7e790acf6476f6c407b7d6c82274e6d65e4d0e57828ea3211ca", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/analysis/nonlinear_static_procedure.h", @@ -3766,6 +3946,18 @@ "exists": true, "size_bytes": 1186, "sha256": "b3fff5fd88e6705fad315201b9b670728247864bac3083745f63d1379d1421a0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "5d1e6cd846e30427fc7a6491383c65ee61055b9e7ce51951f0dfa382a260b628", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -3808,6 +4000,18 @@ "exists": true, "size_bytes": 4405, "sha256": "70d40cadc34ba320943dd353a1e676420380dd270a21de1f09abeb5a965c8de0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6d8af2f255737a17797d60227835116c403d539e267be085de2a55762545666c", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -3828,7 +4032,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7c026cfa42c84c6e6e27d4f2ce5e6f2866c55518af1204d4e98e717baf195d39", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/analysis/solid_dynamic_analysis_state.h", @@ -3870,6 +4086,15 @@ "exists": true, "size_bytes": 8099, "sha256": "a12a5ce2357f47d97f4e97a2a8bd3491dd1608b95611478f30fe39b82cba1da0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a12a5ce2357f47d97f4e97a2a8bd3491dd1608b95611478f30fe39b82cba1da0", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -3890,7 +4115,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "80302bae63f20e93e62da8f3c388b5a95b96fa458cfd918f4c39cd0a7b8b7942", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/analysis/solid_frequency_procedure.h", @@ -3932,6 +4169,18 @@ "exists": true, "size_bytes": 648, "sha256": "8413c4500e8626dbb86e7fcab3fec420e11cd797b843b025237cfa4181632413" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "82a28fb1e35650e318a7bd1218a799cb01925f81ff30999c0bec6fc8c492db17", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -3974,6 +4223,18 @@ "exists": true, "size_bytes": 1705, "sha256": "7c0494f74f53fa40d39f04545e51d88da3090233a4225018e68bb0aebff03f83" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "41675ea1ae2d96a21bb21725589ade95b41c18ee8260973e93c48b46b52de107", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4016,6 +4277,18 @@ "exists": true, "size_bytes": 1813, "sha256": "e82b58299ee5b7b3132da94c75c37a2a2deee5ee3c11b55c59fa3f14f9c768a7" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2111297eff23d265809b69f70025434a299b313b4e4a626425594682d58c274b", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4036,7 +4309,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "3f231a0277b3732fe12d4fabc5c01d8353e9f52133740fddf3ead87eb6bb362e", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/analysis/solid_static_procedure.h", @@ -4078,6 +4363,18 @@ "exists": true, "size_bytes": 1461, "sha256": "6decc731f6f8d966add949a8dcb43a28aca9d826c274834611388050e32d9605" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "c102671f171ceb64692c59a70f1b7f2d135515f59b4a0bb14d673e395395061a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4120,6 +4417,18 @@ "exists": true, "size_bytes": 4688, "sha256": "92c9ef644ad63f45ffcfeea70988ec414c67e39d70b96e3753a1cac1091c65ef" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "8a68ad1738dd75f5cb5f07a112bee0506a050f3a96d45056c99aedb1f57a950a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4162,6 +4471,18 @@ "exists": true, "size_bytes": 4559, "sha256": "4f025f310bfe73ab0b73585d848dd8ef974faee34842c869a37bdff36b507e95" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1a8de48d0c6596d15112e24f03a93780b8a495985521e24f505047ce33204c18", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4182,7 +4503,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "4bd16ce8968b188074be5814b3850653ade6adca1fe044c56ac45c117a530d01", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/analysis/thermal_time_grid.h", @@ -4224,6 +4557,15 @@ "exists": true, "size_bytes": 1678, "sha256": "9bbbadea46ca8a415a1c440ef923c06dea97829512b84d648fe8c98d0c06b3d6" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "9bbbadea46ca8a415a1c440ef923c06dea97829512b84d648fe8c98d0c06b3d6", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4266,6 +4608,18 @@ "exists": true, "size_bytes": 1313, "sha256": "7e077f6748827880f4512b4c687d611572ec3afeefef55ace2a3a4be368bff2f" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "63e337fcb167b10acbe242e8af7451b3fdcba0412dcc8337342aacac0ae898c4", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4308,6 +4662,15 @@ "exists": true, "size_bytes": 623, "sha256": "604c631e180e16dc06ac8d69c1b42de740c372de47d36ff38c99216fa4524e8b" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "604c631e180e16dc06ac8d69c1b42de740c372de47d36ff38c99216fa4524e8b", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4350,6 +4713,15 @@ "exists": true, "size_bytes": 1969, "sha256": "076f928a4cf4f382d4752f9b3d9b2bef72e8c90a29c2fa1e5f1d4978a7a18cac" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "076f928a4cf4f382d4752f9b3d9b2bef72e8c90a29c2fa1e5f1d4978a7a18cac", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4392,6 +4764,17 @@ "exists": true, "size_bytes": 1938, "sha256": "c04fd63116f816c0ca7f400ef94c026c11fee77dfed45827a9e9f17963fdbc66" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "d5c7e2b6de916006f198aa765770232112e633091df4947b0f76f42a540a160b", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4434,6 +4817,15 @@ "exists": true, "size_bytes": 1202, "sha256": "e8e5cfe08a4b632e336f346a6543a65c367441a8d55da4261c63e8bdb24661ed" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e8e5cfe08a4b632e336f346a6543a65c367441a8d55da4261c63e8bdb24661ed", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4476,6 +4868,15 @@ "exists": true, "size_bytes": 1737, "sha256": "7b4c7453126b94dd70206a8ac9b68f9477603765f412c1a0b51a5fba3f0dbbbc" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7b4c7453126b94dd70206a8ac9b68f9477603765f412c1a0b51a5fba3f0dbbbc", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4518,6 +4919,15 @@ "exists": true, "size_bytes": 1806, "sha256": "55280250549b42896d95e03423c2035a607cbccc88eaa3220a3fb0135335d737" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "55280250549b42896d95e03423c2035a607cbccc88eaa3220a3fb0135335d737", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4560,6 +4970,17 @@ "exists": true, "size_bytes": 3940, "sha256": "40770bbc1ae52052d9ca1a099a3f2636939ff50ec6c539436a1fe4f9bef08033" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2500005b6c0a8ddc84820d5e8b7ac27f160f6ce8d0772e27fec38787c7806766", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4602,6 +5023,15 @@ "exists": true, "size_bytes": 279, "sha256": "828c23dbff20c3e8952bbf8e8f527140a2dec6854ef42fbe0902ba91dec8f17b" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "828c23dbff20c3e8952bbf8e8f527140a2dec6854ef42fbe0902ba91dec8f17b", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4644,6 +5074,15 @@ "exists": true, "size_bytes": 2301, "sha256": "75e1d103d73486a760520ac2cd0c774b66813a3e09bfeb13c79dad1feca80b29" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "75e1d103d73486a760520ac2cd0c774b66813a3e09bfeb13c79dad1feca80b29", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4686,6 +5125,17 @@ "exists": true, "size_bytes": 1506, "sha256": "2f1c41393ba515a02caf90671e8e7452d07b2abe8c5787fc7dca9a302b9acaf1" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "c1852e182cc6ca402ffc86fe84d4e9cfe41e4283a88b1436ee29fb4836b293c1", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4728,6 +5178,17 @@ "exists": true, "size_bytes": 1756, "sha256": "be561c0006ce9ba8ca550f8a9a1cb21551eb4a11cc03f103ea341cff4a7df75f" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "c7ee15ecade3e32a1bd9d025b62dbf15030c3655f526ccc7ea728f486d51a713", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4770,6 +5231,15 @@ "exists": true, "size_bytes": 909, "sha256": "febc57c9795882ec62f866254ec0bede935a45e12d44fcac2598a1b65bda969d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "520c5f1622c36224d502ddcf43689c8abe4dd05bd65a64f33cedcbbe9a17797f", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4812,6 +5282,15 @@ "exists": true, "size_bytes": 863, "sha256": "e5f52e6d6c92dd1324bd6df0c7ea1e3af44b6691d13d2834024cbfbe9ee89c34" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e5f52e6d6c92dd1324bd6df0c7ea1e3af44b6691d13d2834024cbfbe9ee89c34", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4854,6 +5333,15 @@ "exists": true, "size_bytes": 727, "sha256": "a4e9d115d47cf19b6a1f6a0eba5f1f7e72c5baf5ca80878e6952594861c1cc00" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "88624b85c5b9b3025d36ca60aa6d2948805447d497d0400f113bbd6f5a6d9226", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4896,6 +5384,15 @@ "exists": true, "size_bytes": 3740, "sha256": "cbdc95b6f51ecc4a18956fa12b415280b6d660a2b50fe757425abcca8b95c8e1" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "87f804e4950639c3143b68945e9fb8f4796095f971e83d216cb1c9c054348d37", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -4938,6 +5435,15 @@ "exists": true, "size_bytes": 3288, "sha256": "37d9efdff7d44267a82493191ae229e264c1dd51850e7c0057f23ee9d6475e3e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "37d9efdff7d44267a82493191ae229e264c1dd51850e7c0057f23ee9d6475e3e", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -4980,6 +5486,18 @@ "exists": true, "size_bytes": 2672, "sha256": "81867692e2cbf359d6345b90a162f0d68fc29a1838066efa1690bc51127a2a42" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "3042b5d97d4dff58354d393d2ca54a0b3e37daf22d4955c40c1fd0cd89378c8a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -5000,7 +5518,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a6dffad05713548237b571c728b8973bb6dfe6aca97e440262a259ff93963025", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/elements/element_definition.h", @@ -5042,6 +5572,15 @@ "exists": true, "size_bytes": 1525, "sha256": "6b4a9f031837de253ffd3f3de95e179caaf56db85679b1e45d089e5c282e4be6" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6b4a9f031837de253ffd3f3de95e179caaf56db85679b1e45d089e5c282e4be6", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5062,7 +5601,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7d6182e3e3099d709736f8355fd9d0fa5b6e447bc3b1498b3ac6846e7c486cad", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/elements/element_factory.h", @@ -5104,6 +5655,18 @@ "exists": true, "size_bytes": 1457, "sha256": "8d072c8109490ef1dcea2c9a0fa108cf56124f41b4679a9bf72f72d2352b77e6" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "19922d777601f6248ac0967f06e852128132300934bf22631e5995516a76c614", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -5146,6 +5709,18 @@ "exists": true, "size_bytes": 5815, "sha256": "d56bbf194142f61fa3485943b692f9773f25786bba45b7019e47717c935f0622" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0e0c604dda5e4506f9e4f059f2067ad2ea02098f83f392df0f04540fc4267a97", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -5166,7 +5741,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "b01d20476b6121510c37904e80eea519f563fe700d287acf430ecff5cedd4c41", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/elements/finite_rotation.h", @@ -5208,6 +5795,18 @@ "exists": true, "size_bytes": 2805, "sha256": "5f598c2e377a6b0f3c23b54ffa561b7e43acf5c0d761b60de51ac07f0c121cf9" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0cbaf90d353d7bcf62db1c81a7786c52e451eee00d36a45b1f81fc8aa81a2acb", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -5250,6 +5849,15 @@ "exists": true, "size_bytes": 2166, "sha256": "daa3fb45c2ee5b1d0d703603a1a45325602d9d6ea5beb00b54413adad9c57f83" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "daa3fb45c2ee5b1d0d703603a1a45325602d9d6ea5beb00b54413adad9c57f83", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5292,6 +5900,15 @@ "exists": true, "size_bytes": 5309, "sha256": "ba517d7862b078f23d2596d86f1ba2087c5731408fa117adf992be98d1624dc3" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ba517d7862b078f23d2596d86f1ba2087c5731408fa117adf992be98d1624dc3", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5334,6 +5951,18 @@ "exists": true, "size_bytes": 10268, "sha256": "54ea1f953bbfe9a99be9d268efd386bfe8da6f7150bb2707a96dc076b2b0a539" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1cf73d6e2a1a75dcd8291071dc3cf52476294aa5b5b7cb5e9655725e8d621865", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -5354,7 +5983,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0ce02c7ae496274ffcd613f89059baa55275c8f0254c7db7c1137716348c4ad5", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/elements/nonlinear_mitc4_inertia.h", @@ -5396,6 +6037,15 @@ "exists": true, "size_bytes": 4377, "sha256": "77efbfe4236776f652010c739e39a8687a478e64e94091a0d86ec98b96f4c55d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "77efbfe4236776f652010c739e39a8687a478e64e94091a0d86ec98b96f4c55d", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5438,6 +6088,15 @@ "exists": true, "size_bytes": 7145, "sha256": "984ad13c87b021047ed9f2e7dfbd0ad5d783a8318f5c01557eb502ba1283c13e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "984ad13c87b021047ed9f2e7dfbd0ad5d783a8318f5c01557eb502ba1283c13e", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5458,7 +6117,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "69a0517d9e8ffc1127aca278d8da38f87c17e30e8dcd3ca90dd9a2f84e7fd327", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/elements/thermal_element_layout.h", @@ -5500,6 +6171,18 @@ "exists": true, "size_bytes": 661, "sha256": "6b83aeb8fffb804bd684c550298bd5004c67ba4fca8f0700c79e7047e7fd7d2b" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "fee498c7d49ecaea798b7d3c60aa82c24cd04f427ca11d443f8cc600d45225b3", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -5542,6 +6225,15 @@ "exists": true, "size_bytes": 3208, "sha256": "89e0d4de26e7f3dcd6f2cc9b122714420274cf0d75d5515ef5c78deacb134bf6" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "89e0d4de26e7f3dcd6f2cc9b122714420274cf0d75d5515ef5c78deacb134bf6", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5584,6 +6276,17 @@ "exists": true, "size_bytes": 6124, "sha256": "72760a6c0909f08e32482c6129937c9d3211ee0104efc570d8b20e2a35d397f7" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "f7b374312434b931a3c9eeb5e48c0686a8c155e700a1ca17ff94cb066ac24159", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -5626,6 +6329,15 @@ "exists": true, "size_bytes": 824, "sha256": "566469c00568a30a37ab19de061e662e0f929a04d4684dda4aaa8a6e008ccf3c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "566469c00568a30a37ab19de061e662e0f929a04d4684dda4aaa8a6e008ccf3c", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5668,6 +6380,15 @@ "exists": true, "size_bytes": 756, "sha256": "bd4543cea36599ea92257c1e88cd4a77a9c9c5eb1192abcdcf6833dcac664bb5" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "bd4543cea36599ea92257c1e88cd4a77a9c9c5eb1192abcdcf6833dcac664bb5", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5710,6 +6431,15 @@ "exists": true, "size_bytes": 1258, "sha256": "3f0ca1ba9a387980eccb26dbd3a56218c615bd64308c4541f5a23676a3db0e01" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "3f0ca1ba9a387980eccb26dbd3a56218c615bd64308c4541f5a23676a3db0e01", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5752,6 +6482,15 @@ "exists": true, "size_bytes": 813, "sha256": "94eefd90f976441d4b6ff5c636a9aceb83a5837a680af37e0061c0208f2aada0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "94eefd90f976441d4b6ff5c636a9aceb83a5837a680af37e0061c0208f2aada0", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5794,6 +6533,15 @@ "exists": true, "size_bytes": 862, "sha256": "43d0eab0f39eab6e1d5152d0c48083eef30bd1fc54a06ebf3889928decafbab5" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "43d0eab0f39eab6e1d5152d0c48083eef30bd1fc54a06ebf3889928decafbab5", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5836,6 +6584,15 @@ "exists": true, "size_bytes": 1171, "sha256": "0bdf904113abd95a2bac4580dbd716112651ec8ac77009316bae33e42bd2b24d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0bdf904113abd95a2bac4580dbd716112651ec8ac77009316bae33e42bd2b24d", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5878,6 +6635,15 @@ "exists": true, "size_bytes": 953, "sha256": "53029e79767849429117899efa855d70ab74901075cec317a7caf0175cc4a399" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "53029e79767849429117899efa855d70ab74901075cec317a7caf0175cc4a399", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5920,6 +6686,15 @@ "exists": true, "size_bytes": 1005, "sha256": "2081c4c40c0d8c9caf469c9848588b732b221c7c6d743c67ff59d2a714857006" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2081c4c40c0d8c9caf469c9848588b732b221c7c6d743c67ff59d2a714857006", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -5962,6 +6737,15 @@ "exists": true, "size_bytes": 780, "sha256": "9deaa76211f32fae7f0f38212770699dacdfc058da733bf597b18729fb92a5d1" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "9deaa76211f32fae7f0f38212770699dacdfc058da733bf597b18729fb92a5d1", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6004,6 +6788,15 @@ "exists": true, "size_bytes": 2727, "sha256": "e43057535d8604d797cd9365e4be0261d659a54c7210139ab593d9572ee63e24" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e43057535d8604d797cd9365e4be0261d659a54c7210139ab593d9572ee63e24", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6046,6 +6839,15 @@ "exists": true, "size_bytes": 1487, "sha256": "09fee88d3161dfdcaa9e21d20d8da1ae1ad58e327435fdb4f71003a7bfa8503c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "09fee88d3161dfdcaa9e21d20d8da1ae1ad58e327435fdb4f71003a7bfa8503c", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6088,6 +6890,15 @@ "exists": true, "size_bytes": 1593, "sha256": "18c18ac67e1b0f06507eeaf6736bcbe98e6da831b06f39a7d77ee207b8ce0f21" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "18c18ac67e1b0f06507eeaf6736bcbe98e6da831b06f39a7d77ee207b8ce0f21", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6130,6 +6941,15 @@ "exists": true, "size_bytes": 3059, "sha256": "91f5bf5e2519b4c70d8bb6e3d777a959add1fd1b9b50d2948eba0269cc5a2f62" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "91f5bf5e2519b4c70d8bb6e3d777a959add1fd1b9b50d2948eba0269cc5a2f62", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6172,6 +6992,15 @@ "exists": true, "size_bytes": 1737, "sha256": "142ce86928036e9ca33e2677035ecdf4ab28190bec484a354703b2b3b606ca91" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "142ce86928036e9ca33e2677035ecdf4ab28190bec484a354703b2b3b606ca91", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6214,6 +7043,15 @@ "exists": true, "size_bytes": 864, "sha256": "82adaac6057450179526136e019df159c086a4728236d764e7455a8d88646ea4" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "82adaac6057450179526136e019df159c086a4728236d764e7455a8d88646ea4", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6256,6 +7094,17 @@ "exists": true, "size_bytes": 1956, "sha256": "6df69fe0ada766d3ed54bc85597549a6080919b6b4b2c8d19c148928a0c542eb" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ae39f3c1ed16c3ed8b68efec897bb1c6a61d6492e22d85738648469f545fa916", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -6298,6 +7147,17 @@ "exists": true, "size_bytes": 2693, "sha256": "e5070e0f2e6cef8d3600c2f53caca0104077351c75fb91ca821daf01e914a2e4" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "4629e887f953fa85568c7ff575907129811d431e7bd9584dbdcf631a41ec1afb", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -6318,7 +7178,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6b08b1bc23d79c47b9711d357b8d10335c9b9432c17cbe2efa8b12bcbf993054", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/math/vector.h", @@ -6360,6 +7231,15 @@ "exists": true, "size_bytes": 1990, "sha256": "8b42c3229093805c40f22e5f4ceeece03809b45dbe389079daf3b6d6a366bbde" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "8b42c3229093805c40f22e5f4ceeece03809b45dbe389079daf3b6d6a366bbde", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6402,6 +7282,15 @@ "exists": true, "size_bytes": 4144, "sha256": "6c7496bf7c196a5a447c6e632d0d585e4280639f6f97d9b6e4819940703d13cf" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6c7496bf7c196a5a447c6e632d0d585e4280639f6f97d9b6e4819940703d13cf", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6444,6 +7333,17 @@ "exists": true, "size_bytes": 12382, "sha256": "0b0ae444e1b86e71e4aa3e9459cd36b03181f9547eab600449a2e6d0a28781bb" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "34e58b0638248bc6b058ec11d81d27a8e9f597efb1bca4f313483c2912cc08c2", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -6486,6 +7386,17 @@ "exists": true, "size_bytes": 15398, "sha256": "f667753748daf3f89b1a58b5c54198aa496c5ff2849f4050c720ca4daec99e92" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "b2179579e7cd16b590f97234b59c2ff93eedbd19d174665ee7baac3835d33404", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -6528,6 +7439,15 @@ "exists": true, "size_bytes": 1865, "sha256": "4efdf38a5a1da4ca3d4cc1ffa2a0454db58fec2445ded4ca41bb5d2373df3049" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "4efdf38a5a1da4ca3d4cc1ffa2a0454db58fec2445ded4ca41bb5d2373df3049", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6570,6 +7490,17 @@ "exists": true, "size_bytes": 2686, "sha256": "52e5faa473878a15e89bfcd4c201a4dd745b1b37c77ead4a356a17d647de0975" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "f63f68354d3d8629d911598272e9b90b2201c8d5a7cb3abab2d53fc2ba97bc89", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -6612,6 +7543,15 @@ "exists": true, "size_bytes": 941, "sha256": "cd7806d43a8cbad8329d63b3a4e693701256ccc8042e6862a9e0a11d885229f6" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "cd7806d43a8cbad8329d63b3a4e693701256ccc8042e6862a9e0a11d885229f6", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6654,6 +7594,15 @@ "exists": true, "size_bytes": 3445, "sha256": "773e8310785fd29707b724925580d8ea35f1200692ee398cbb779b933161643b" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "773e8310785fd29707b724925580d8ea35f1200692ee398cbb779b933161643b", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6696,6 +7645,15 @@ "exists": true, "size_bytes": 2247, "sha256": "0c3af7fc5cda72282b0a6ad7977121d2cdac9482f41a3fda198eeef182a95f95" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0c3af7fc5cda72282b0a6ad7977121d2cdac9482f41a3fda198eeef182a95f95", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6738,6 +7696,15 @@ "exists": true, "size_bytes": 1412, "sha256": "259859794ca6ac022da7aee01ad816c8cc7e6dc0c13a4ceb0c9c5ceef86a8625" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "259859794ca6ac022da7aee01ad816c8cc7e6dc0c13a4ceb0c9c5ceef86a8625", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6780,6 +7747,15 @@ "exists": true, "size_bytes": 845, "sha256": "663f1f9ecf26e67b24338cc4e8e9e5ddbc704bd09857c46c99734457462067ef" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "663f1f9ecf26e67b24338cc4e8e9e5ddbc704bd09857c46c99734457462067ef", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6822,6 +7798,15 @@ "exists": true, "size_bytes": 907, "sha256": "c2273ba002eef83d851fb16d5bca47fe3c3c63d63c56634a579322aa4426e6ae" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "c2273ba002eef83d851fb16d5bca47fe3c3c63d63c56634a579322aa4426e6ae", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6864,6 +7849,15 @@ "exists": true, "size_bytes": 1246, "sha256": "af86ad310b6f90728fbcb61763f80d70f64f6b201401b84054796dfbbce69a25" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "af86ad310b6f90728fbcb61763f80d70f64f6b201401b84054796dfbbce69a25", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6884,7 +7878,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "61d255d8aad845e2224a5ce1095b2162009b5a0a43742ebcab61e899a6c9e155", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/results/result_records.h", @@ -6926,6 +7931,15 @@ "exists": true, "size_bytes": 2521, "sha256": "0556d284df3a9a276f58fcb9f470154c927b69261c860ae8f791086326521233" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0556d284df3a9a276f58fcb9f470154c927b69261c860ae8f791086326521233", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -6968,6 +7982,15 @@ "exists": true, "size_bytes": 2760, "sha256": "812bc148586faa45671cfbca0d003b33294f29a36fbb885dd20f863634028f42" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "812bc148586faa45671cfbca0d003b33294f29a36fbb885dd20f863634028f42", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7010,6 +8033,15 @@ "exists": true, "size_bytes": 856, "sha256": "cded14565a889f90c1cbd79762f59034acb3b028bb312d6df105a215ebf183a9" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "cded14565a889f90c1cbd79762f59034acb3b028bb312d6df105a215ebf183a9", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7052,6 +8084,15 @@ "exists": true, "size_bytes": 956, "sha256": "a4f6a0825b2683da20e56006e6d032c7c94dfab4d8e6b9aae9269a459a702a4c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a4f6a0825b2683da20e56006e6d032c7c94dfab4d8e6b9aae9269a459a702a4c", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7094,6 +8135,15 @@ "exists": true, "size_bytes": 1462, "sha256": "8ecb20c3af801d4931fba64827eb98cedf0a0e5bde901dd83f218ae199d68a68" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "8ecb20c3af801d4931fba64827eb98cedf0a0e5bde901dd83f218ae199d68a68", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7136,6 +8186,15 @@ "exists": true, "size_bytes": 1786, "sha256": "40a952f61109804a048cc8df5fa6f23360757c7c20f631d3165a64d13b550af5" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "40a952f61109804a048cc8df5fa6f23360757c7c20f631d3165a64d13b550af5", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7178,6 +8237,15 @@ "exists": true, "size_bytes": 896, "sha256": "fdb42f1bd4708873f5c05affd17249b5973d5bf09ebbebda75a9fcf5b631e753" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "fdb42f1bd4708873f5c05affd17249b5973d5bf09ebbebda75a9fcf5b631e753", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7198,7 +8266,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2e4234769aafa9ea700bf1ff553aebdac4a135240d19f70063a4898cb0bfe32c", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "include/fesa/solvers/eigen/mkl_feast_eigen_solver.h", @@ -7240,6 +8320,18 @@ "exists": true, "size_bytes": 3945, "sha256": "af1550d9eea9e93c3944925bc4f8a5b450f850cb5678ba76c38d69a3dd93d220" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "072bdc1aa46fb64d231d9fe1a88fa98bebbe4060fb5010787858f881ce03df8a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -7282,6 +8374,15 @@ "exists": true, "size_bytes": 992, "sha256": "76fb68372a0ea1a290a7ee776b77d67fd520e0d5efe5603c2dddb93014607090" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "76fb68372a0ea1a290a7ee776b77d67fd520e0d5efe5603c2dddb93014607090", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7324,6 +8425,15 @@ "exists": true, "size_bytes": 926, "sha256": "7cdf2f5402ee1b9f2c313e8fb35973c81c3b172bbac19f3ab4e7505fc4724ebc" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7cdf2f5402ee1b9f2c313e8fb35973c81c3b172bbac19f3ab4e7505fc4724ebc", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7366,6 +8476,15 @@ "exists": true, "size_bytes": 1009, "sha256": "e97277540a4bb707e4006b8284806b51b6a55b7827ff8071a17ece337333f771" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e97277540a4bb707e4006b8284806b51b6a55b7827ff8071a17ece337333f771", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -7408,6 +8527,15 @@ "exists": true, "size_bytes": 1043, "sha256": "605ea56d58789f986eafc7e8025d663e58d132f553286754ab20b242625ff4c1" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "605ea56d58789f986eafc7e8025d663e58d132f553286754ab20b242625ff4c1", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -12230,6 +13358,15 @@ "exists": true, "size_bytes": 7485, "sha256": "1f2e53a87e187702d6dbd989253e21bbbe63a5ea2b53bd8bde5295f2d8a8be45" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1f2e53a87e187702d6dbd989253e21bbbe63a5ea2b53bd8bde5295f2d8a8be45", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -12250,7 +13387,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0229ebfb74b980663c49cf4b5aae499d000acbfa783fd0678baf3cd38690c21f", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/analysis/analysis_state.cpp", @@ -12292,6 +13441,18 @@ "exists": true, "size_bytes": 8298, "sha256": "b3ccefebacc94637023f084de5045dc71a6756c352489fc65db7c597600c7b8e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "52b659718f78d1e5ab21cf344b85b773ea71883a3ec8f1196f5458b053cfa1b0", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12334,6 +13495,18 @@ "exists": true, "size_bytes": 18192, "sha256": "598015ba5c07c5d6c10df3a635aa3f1afdc345081a690d93ec38382eb13afac5" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "f6192e7d0bdc3a4755b9a80fdc9a9cfc6ceb05d8bf92cf843f901ed9b4e4efc2", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12376,6 +13549,18 @@ "exists": true, "size_bytes": 18464, "sha256": "ca6953cbbc0a18670ade80e43f578344ffb329a1aa1827d4fd4c70889daaea40" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "043f3aff7eee3318fd48e9ecb6974296ce1f3779a2e25bb7ee8a128a1666eb66", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12418,6 +13603,15 @@ "exists": true, "size_bytes": 1923, "sha256": "2c99e7a5972a6696bb4b46888ff53da6cb94a9ef4784fd28f3d31d5552d37cdf" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2c99e7a5972a6696bb4b46888ff53da6cb94a9ef4784fd28f3d31d5552d37cdf", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -12438,7 +13632,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "b0ff1b287b591bffa422a639c36abbdbbb5c9421ac0b47bda60bd20fb5b02472", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/analysis/hht_dynamic_algebra.cpp", @@ -12480,6 +13686,18 @@ "exists": true, "size_bytes": 37992, "sha256": "737597edbb5511d998c57fa44a7eb889310aa163f77028ae3ab0bec268e94044" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0b8595c6bde242b519defddda400334687a7054bc201e20613dc9d6d50e26137", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12522,6 +13740,18 @@ "exists": true, "size_bytes": 34619, "sha256": "383550a38f5a2636f49d40f9a17e28ed05cf228a2f7318d13edda49bbe7cfdf0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7d7c541131fa4b050b775111f94b3521d53c89c5cb12855283247d00448ae77c", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12564,6 +13794,18 @@ "exists": true, "size_bytes": 7913, "sha256": "dacc5d797e3ed1089a86b8b90c727a16aafc368f3acda3526993aa7476565736" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0b5f73b574a5a92e829a5242c6614caf189b3d773687ee146ed34c7807671654", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12606,6 +13848,15 @@ "exists": true, "size_bytes": 684, "sha256": "99c1d22d51d7729fb973bf1cf1b5c60ffad0c38d2bce98033674c7a5e67fc8ee" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "99c1d22d51d7729fb973bf1cf1b5c60ffad0c38d2bce98033674c7a5e67fc8ee", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -12648,6 +13899,15 @@ "exists": true, "size_bytes": 3779, "sha256": "88a53eb7b14a07003da29980be3b9beb3725d1a8bad3dc120e98ca28e017fb97" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "88a53eb7b14a07003da29980be3b9beb3725d1a8bad3dc120e98ca28e017fb97", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -12668,7 +13928,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "c24755a15a407827ee0595da155cfb2c0dd3b69ceaf73eaf2223d5cc4d7908ab", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/analysis/hht_reduced_internal.h", @@ -12710,6 +13982,15 @@ "exists": true, "size_bytes": 10236, "sha256": "53149a7af8e159ac73f48ae9a4f2925a1fcc73abfea014380fe3eb4f17dd1470" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "53149a7af8e159ac73f48ae9a4f2925a1fcc73abfea014380fe3eb4f17dd1470", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -12752,6 +14033,18 @@ "exists": true, "size_bytes": 69872, "sha256": "ce8d4785d3073f8b55d9aa7ab9d24784aff30e8aa49f6a66046cd34074566baa" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6baacadd40d472ece2dcfc7fecf60227b9d5ff96ce1ed1cbed48176f1c752dfa", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12794,6 +14087,18 @@ "exists": true, "size_bytes": 54040, "sha256": "8de5585a8ac7d94a90499e39fbfab251f037387a77564eb8d8101277bec234eb" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "927fa94d1836ad0b53ee8a03bace9c3fae63471a69fadf47f63ae5dd06dee7b8", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12836,6 +14141,18 @@ "exists": true, "size_bytes": 830, "sha256": "4c7d98491f698f10c3a1611fbef36390ccd93e6fe27682ff0e4a6f9e806cdc0a" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "b2b6f50b206caeb0f9d74866b2c59e9e38fa701d974572bd4ec14f8579bb0448", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12878,6 +14195,15 @@ "exists": true, "size_bytes": 4080, "sha256": "31c33d2c90a6427b7b699cf1b07cbadc66b02a31bd959516b60c96c325c8f9ba" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "31c33d2c90a6427b7b699cf1b07cbadc66b02a31bd959516b60c96c325c8f9ba", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -12920,6 +14246,18 @@ "exists": true, "size_bytes": 19831, "sha256": "95208910a4df1b6c26ff4f5247fd596197e02ac7081ff02ffd060b213d21e27e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6e550854aa4cfdf0398b11f751bc9e60dd4dd292716493f4d2077f910d620577", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -12962,6 +14300,15 @@ "exists": true, "size_bytes": 1288, "sha256": "46411827221f04928eb3221ea1e8e89a5de530cb0c844adae071cd02aef34302" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "46411827221f04928eb3221ea1e8e89a5de530cb0c844adae071cd02aef34302", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -13004,6 +14351,18 @@ "exists": true, "size_bytes": 7062, "sha256": "cdbf0b3c26a3be304c586d3ae0a9cb184c29e6310862014aa8c83808275f7268" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7dc57443c9ebb950cde0b6ee05b8392c53ffbd415aed4ddddca42c5a8005a959", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13046,6 +14405,15 @@ "exists": true, "size_bytes": 22698, "sha256": "28897eda1e5b3d303199c50b7cce2303506a7e49608c4dd13d10726aebad6edc" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "28897eda1e5b3d303199c50b7cce2303506a7e49608c4dd13d10726aebad6edc", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -13088,6 +14456,18 @@ "exists": true, "size_bytes": 13323, "sha256": "a4d2127c9c6b1e794979889165aae8f1fc26d0ee172c0f336f4bf3f60dee5e91" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a8950c19928d66ad16d481711da56f84deb49317e2872f7cb389267a04d0d834", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13130,6 +14510,18 @@ "exists": true, "size_bytes": 23358, "sha256": "fe8e613fe3f7b52e21334576c63bd3be8125c749f5e3c3c0a9897ecbc304f82c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0bf0362bdff08dfba687412b569aa53dbe2c2fb43a3f0a4a2a10ec688e145cf8", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13150,7 +14542,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "983dfbfa48afa2b079a6af168bc4999ddf5bd296064a072c42526999d1b59111", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/analysis/nonlinear_static_procedure.cpp", @@ -13192,6 +14596,18 @@ "exists": true, "size_bytes": 44130, "sha256": "a65bff3c60608eaa2d7e2ccdecaaa80d8f4f529f1e9ed37e2afc68f12eda5c08" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "67c983ccc020c5ffbcc28b48feed1aadf5d5a98ccd57d917ce07afde74c21d24", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13234,6 +14650,18 @@ "exists": true, "size_bytes": 11191, "sha256": "6f18640f6f92fb68d0b3512d8e8c59740517257bf52ef94a510a7805a80a7357" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "69f2f675a77e6522fbefe834ce4cc5ff07bbdb68040a10350ff464f89bbe680c", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13254,7 +14682,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "d88c3320b0ad5e702c7c329bffe990097bfab9b3cf8ab21b6ec38029c2f9bfb7", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/analysis/solid_dynamic_analysis_state.cpp", @@ -13296,6 +14736,18 @@ "exists": true, "size_bytes": 13670, "sha256": "358a94f866c01bedc07955dd6fb0f1ce68074995d0793f4f001e5cafba3319ef" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "353978bfc08bfd06097ad726eb599d81143d1b2ad22bb5f1fedd580799419ecd", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13338,6 +14790,18 @@ "exists": true, "size_bytes": 18995, "sha256": "6459820a8886291ba52e5be10c7cceb7dbe179d9d18530c7d7940773fd11e9c5" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "8bc27912d6bf0b9faf9a5b696466680a6223fd0de1b6430a5c0166c67d0fece1", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13358,7 +14822,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ac17c9183cd1d34b4fec7195ba18ff38e03e6cf5288eec9a747fd471ba63bfb3", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/analysis/solid_hht_dynamic_procedure.cpp", @@ -13400,6 +14876,18 @@ "exists": true, "size_bytes": 41587, "sha256": "00d0cf74938f7ce8c1033c83685f6a9d03e6e88afdd1999fcbcd272feb945433" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "3e37a8c198964fcd812737a39de7377117ed1b7d5ef705b61f14530871159bc2", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13442,6 +14930,15 @@ "exists": true, "size_bytes": 8692, "sha256": "e326852240b431dbe5a5edbeb5ba02e74840c2d5d7b8fbd471ee8e314a114487" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e326852240b431dbe5a5edbeb5ba02e74840c2d5d7b8fbd471ee8e314a114487", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -13484,6 +14981,15 @@ "exists": true, "size_bytes": 8571, "sha256": "b4aac9f0be4ce3da337e00af9668e0bcbee9b9a121accff0816a601d6bc9ec3b" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "b4aac9f0be4ce3da337e00af9668e0bcbee9b9a121accff0816a601d6bc9ec3b", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -13504,7 +15010,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "80ac84463a46f15c5f3b44061170218db2e44affd7032f21fd722ff2d2ecaeae", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/analysis/solid_hht_representability.h", @@ -13546,6 +15064,15 @@ "exists": true, "size_bytes": 9648, "sha256": "d6bdc01ef05ac8a78a54d6089f1e4c65f4743b1f8d06465c861d315ce1dfa2ba" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "d6bdc01ef05ac8a78a54d6089f1e4c65f4743b1f8d06465c861d315ce1dfa2ba", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -13566,7 +15093,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0a1fb4b341a7d150c04ff65a6bf894e6bab8e8eeecb8ac3c86c1d2fb400e9762", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/analysis/solid_static_procedure.cpp", @@ -13608,6 +15147,18 @@ "exists": true, "size_bytes": 22778, "sha256": "15b2804a11ab2161bb39439e810143612e4467c861f0b1c05ef98cc61c5c5572" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "cbf14e888bbdd2dbbb1fbe1926eaf0328702c862018e824c364a221b048fced9", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13650,6 +15201,18 @@ "exists": true, "size_bytes": 10053, "sha256": "9f5269cecda39701eec3078a77aea4932626010ed38d3d146230e384cdcd763d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ef59ff9429bb9981152d4993cdf8152e0711cba86246ccae20de995e6ebca9b3", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13692,6 +15255,18 @@ "exists": true, "size_bytes": 9534, "sha256": "9e1d88f50516239954f1816d1418c1b0a1651c66267ace846c24fd58b90011ed" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a233c9d402ccd34b252bc51df1b2cc36730a2424ea4e8f7c3ed692f2bfeeae0a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13734,6 +15309,15 @@ "exists": true, "size_bytes": 9361, "sha256": "13bc38d26a6f6c310a8468fe1e4bda93544751a5a0e92886cccd00959f5a3bf0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "13bc38d26a6f6c310a8468fe1e4bda93544751a5a0e92886cccd00959f5a3bf0", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -13776,6 +15360,18 @@ "exists": true, "size_bytes": 25286, "sha256": "e73d1283e35fbac5a623be8e383c4d226296bc0ed277ac5e9a8ae5ef3068a5ea" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "f24391c91d2a20fa7e4091aed984fc4b57ed2be73978b34f8a87ac0c73138e3e", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13818,6 +15414,18 @@ "exists": true, "size_bytes": 4024, "sha256": "6100584f6deaafab780c9493978f3c7a45654fc78fa7330ebff73f5a93ceffa8" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "563230d9dc566df3cb55d5448d5406f6167346927cc1f618fc19bea40128e1c7", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13860,6 +15468,15 @@ "exists": true, "size_bytes": 3692, "sha256": "55dd697c3b432fa435a5b1bc40c105895eaeeeff7c2c23b40da9f20377c90f98" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "55dd697c3b432fa435a5b1bc40c105895eaeeeff7c2c23b40da9f20377c90f98", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -13902,6 +15519,18 @@ "exists": true, "size_bytes": 1661, "sha256": "25bab066a53e789c2db57a58da2da628e4952f01a973e04642e0317f18398808" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "02f67b401ee4a182b8709778a674129637a3897fdd6a514cdeb1699ad5736cfc", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -13986,6 +15615,15 @@ "exists": true, "size_bytes": 6089, "sha256": "d8855ed2453e54065a08cddaa4bc79f66b79c2024b8a428d52c03e761b5ea5cc" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "c6eeeac1ffd983debbaaad3bcaf48038d15a6a6113734a8c51e3e2ff41e0cbbf", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14028,6 +15666,15 @@ "exists": true, "size_bytes": 495, "sha256": "3e67c26faaf2f03af51820d44f19b793ffe24a14b428774f334954a77349c15c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "3e67c26faaf2f03af51820d44f19b793ffe24a14b428774f334954a77349c15c", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -14070,6 +15717,15 @@ "exists": true, "size_bytes": 4101, "sha256": "737d7fc12e10f64758a68e7d32aeba8f966e11680009b0c0e4d2fe48891e8d4f" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "737d7fc12e10f64758a68e7d32aeba8f966e11680009b0c0e4d2fe48891e8d4f", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -14154,6 +15810,17 @@ "exists": true, "size_bytes": 15466, "sha256": "5d62f922829b909aa585c31ec73d6aaf21a6a0a092328ddda234c01ba0f0bac0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "72e2eb0ac66d21ffa91490491f0e1f7a2bc5da2421791ca8b5cd254cb14f4130", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14174,7 +15841,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "901ae7fc9ab9de201c06c24e0fe7bd6f0abed87c1198ba113f55c0aab601cfde", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/assembly/parallel_for.cpp", @@ -14216,6 +15894,15 @@ "exists": true, "size_bytes": 912, "sha256": "ed6e9e0ead83f0ba09f4c43ade1961e1ca98e3adc9a64d3e4a68dcbb8c6d05ba" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ed6e9e0ead83f0ba09f4c43ade1961e1ca98e3adc9a64d3e4a68dcbb8c6d05ba", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -14258,6 +15945,17 @@ "exists": true, "size_bytes": 6149, "sha256": "5c8206b81751a9aa5e09b6b351dfd49e0c37af14cb34342aa998b52e486d7cd3" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "c5d2053b04a1cea45f2fcf4e8f09fc246e3d7965acd7e594d5fcf07e97d47bd6", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Local matrix append extracted to ordered contribution helper; ordering requires independent confirmation.", + "numerical_equivalence": "not-claimed" } }, { @@ -14300,6 +15998,17 @@ "exists": true, "size_bytes": 12843, "sha256": "bb6c68170df8a5082f0d3f9b126f9fe6135b9252ae2e87353134fcb4c5331b2e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "ba108f6ed38c7940663599029dd70e865d3a49bcb248dac4f295e144a2a5f407", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14342,6 +16051,17 @@ "exists": true, "size_bytes": 16796, "sha256": "81ce889baef2dc9b1027b8d660db4be11967504db3e203a9aa05e97967fca6a8" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "596bc2cfd0fb7829593399374703cb666654c14883dc562bb4c6f33e9e0ba4ae", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14384,6 +16104,15 @@ "exists": true, "size_bytes": 241, "sha256": "9b3cafb2c50fff8dd6cfab1527051da12122f6cd5c8b1859027a5470c2534c16" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "9b3cafb2c50fff8dd6cfab1527051da12122f6cd5c8b1859027a5470c2534c16", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -14468,6 +16197,17 @@ "exists": true, "size_bytes": 6476, "sha256": "985512458c5ffc96164301ed56e608ee198f227c814045d92e2f626b16768200" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a671fc9140ddd2d382cd7d5e00939ef6f3d1a57eb2884f7599ac7f5b2baf3dc6", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14510,6 +16250,15 @@ "exists": true, "size_bytes": 4091, "sha256": "e7542376b043a967293e2bb8eb7c641050cae9c55219ff5e3545586c685602b1" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e7542376b043a967293e2bb8eb7c641050cae9c55219ff5e3545586c685602b1", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -14594,6 +16343,15 @@ "exists": true, "size_bytes": 1452, "sha256": "e04c686ce4f1d07e059db58b6c191185820625d62ef64a2e0f007b470925891a" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "5dd703481f5bccdca9da88c61b30ac0fb672b3bc6303164540992e3b56b8f1d9", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14636,6 +16394,15 @@ "exists": true, "size_bytes": 742, "sha256": "07b2e69069d258dd3ae9b61b45621e238cc7520dcb5a5bbff88f1778c037bcac" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "07b2e69069d258dd3ae9b61b45621e238cc7520dcb5a5bbff88f1778c037bcac", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -14678,6 +16445,15 @@ "exists": true, "size_bytes": 1009, "sha256": "be11a334a7488848a977483e2c86ec2ff54511bd2d45ebd21372b8dc08562269" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "be11a334a7488848a977483e2c86ec2ff54511bd2d45ebd21372b8dc08562269", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -14740,7 +16516,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2e05773ed99829044da9b6f47c20c7f5cd6a73dcef875f9418ada53249289d8f", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/elements/element_factory.cpp", @@ -14782,6 +16570,18 @@ "exists": true, "size_bytes": 13897, "sha256": "8fc8b2d598977c1f94634d2fbfab32159e0449aa50cf3780aeb8ca0ad18e57f5" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "52a47ba5b3a7ddf950ce7233ceecf3a8b23697bd55fe30dc7b76c90a85d79bc0", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14824,6 +16624,15 @@ "exists": true, "size_bytes": 26329, "sha256": "307b19d5efce16f54ab3174a211a1b22abb5c324ae04a9577a1129322277460f" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "307b19d5efce16f54ab3174a211a1b22abb5c324ae04a9577a1129322277460f", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -14866,6 +16675,18 @@ "exists": true, "size_bytes": 10472, "sha256": "65b25e442818c9d1c506f96091c994e7dc5ec91df2243a70af3a0c4f4074685a" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "c2f17c16cd4b23ed0a4cfae990b425a89f09c56b74dd342caebdca0d56da3722", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14908,6 +16729,18 @@ "exists": true, "size_bytes": 26221, "sha256": "e852928046324a0aee3a7117448460ff5a33a61acf36f163e704191e574bfa34" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1d39b4bcdcb0d21b848c5e80bc9a63eefd0e516cce325e02b3c296c38894c469", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Jacobian explicit inverse/gradient multiply replaced by LU transpose solves; reciprocal finiteness gates retained in new execution graph.", + "numerical_equivalence": "not-claimed" } }, { @@ -14950,6 +16783,18 @@ "exists": true, "size_bytes": 10391, "sha256": "b1d1006bb226b320aa8e6c55396cf3af0d57e68548cbcd781defb24e324f0abf" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e8b6dbec546e1d2e93b47caaca69abebec3a5a884edbe586a2b79ab60cf9f9e1", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -14992,6 +16837,18 @@ "exists": true, "size_bytes": 1724, "sha256": "2fe9830c84713c3e8d3d6d84174be4d2edd3fb2add007825e5da056451cb7388" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "68c41d36c69753e27b457db4376841fb1152ff809fa2dcc87cef5cf7f532a81e", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15034,6 +16891,18 @@ "exists": true, "size_bytes": 16771, "sha256": "5109a78a43da9fecb1b69fc5a45a676775992489fd7c6574f312900f3b6f2523" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e14ec91139cb270a6af073df057700813972650caa0f8af0342ba2fc16fad510", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "AddPair/MultiplyPair shortcuts replaced with shared HighLow Add/Multiply.", + "numerical_equivalence": "not-claimed" } }, { @@ -15076,6 +16945,15 @@ "exists": true, "size_bytes": 771, "sha256": "330003d7a9dc556ee09cc20f79408d76cec96f65393127cecbb637fd0830faba" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "330003d7a9dc556ee09cc20f79408d76cec96f65393127cecbb637fd0830faba", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -15143,6 +17021,18 @@ "exists": true, "size_bytes": 4209, "sha256": "143be36ec377adceccfac2526416201c4c56003ff8e314bd8ce80d1e9ac62301" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "51ec8202920bbe2f0c19019bda888f75717cf446aea4f1998605054b854e7bd2", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15185,6 +17075,18 @@ "exists": true, "size_bytes": 14212, "sha256": "9830e87c17571e33a1f8100c782d3b244360cb81f67617f78dc6a3dcd763bab6" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "8d992defadeef53f6dcd832955f382005d0119ca7787bc7cbdec6359db02e6a3", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Exceptional and mixed-zero packet fallbacks remove pair_shortcuts routing.", + "numerical_equivalence": "not-claimed" } }, { @@ -15227,6 +17129,18 @@ "exists": true, "size_bytes": 5575, "sha256": "7490138df171a0a34df533b2fbed431cabaae6fe69b48c15dce408f8044d35c1" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "978fa504dde5a12e07c6b9a95c821f46d6dcea4ec2fc66ce9050f040b96dd845", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Scalar lane fallback and geometry/material packet graph now shared HighLow Add/Multiply.", + "numerical_equivalence": "not-claimed" } }, { @@ -15269,6 +17183,18 @@ "exists": true, "size_bytes": 50264, "sha256": "fdbbd15e961331f14a73fc0bd13932d3f15213cfe91962bf45f1d2faf0a83f79" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6361a420545ab3717c412594846043e85abc7f47d89cd95be42cf164a6a23f4e", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Reciprocal-basis Cartesian strain reconstruction replaced by left/right LU solves; scalar scale loops now Matrix::Scale.", + "numerical_equivalence": "not-claimed" } }, { @@ -15311,6 +17237,15 @@ "exists": true, "size_bytes": 787, "sha256": "2def3eacdffde8680b12a400da54de9d5dd637f59664abfc26b3c7170e115a87" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2def3eacdffde8680b12a400da54de9d5dd637f59664abfc26b3c7170e115a87", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -15353,6 +17288,18 @@ "exists": true, "size_bytes": 45382, "sha256": "5b77592b9070a88484ec8c6d4055e3e50f0bb0e4419af801da81b4efe9fb7154" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e2018e6237ac67c1a24301d910b60e00483ef785ccd9983dc780f36eb3689598", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15395,6 +17342,18 @@ "exists": true, "size_bytes": 83766, "sha256": "74bc4079c9ffdb4083f9a63cae56493a8d1f207be2984919b04815c981aa7a03" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "60e704d7b709339755975351df145b7bac3939b5431541056a4632f5a62ebecf", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15415,7 +17374,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "fb1608b66157dd7aa71a17c4b45423295d96eb384090cdff4d276cefa9e56f75", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/elements/thermal_interval.h", @@ -15457,6 +17428,15 @@ "exists": true, "size_bytes": 4824, "sha256": "913e036c3ed9fef95b7123b8edf57c3bf25be7027819e1ab2177565fefb8cd67" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "913e036c3ed9fef95b7123b8edf57c3bf25be7027819e1ab2177565fefb8cd67", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -15499,6 +17479,15 @@ "exists": true, "size_bytes": 10609, "sha256": "e90d9b24859313d5ba46338240fca448ec4e40cb3f73cb95adefffdbcd1f6667" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e90d9b24859313d5ba46338240fca448ec4e40cb3f73cb95adefffdbcd1f6667", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -15541,6 +17530,18 @@ "exists": true, "size_bytes": 19028, "sha256": "dbcf7cf920ecc685ccd341f89a33a58c10776c6a869d7889bc291f060d1cf15e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1e7795d29dfa509db775c533b791329618f8eb46c4ede18468a0a72737bc181c", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15583,6 +17584,18 @@ "exists": true, "size_bytes": 2256, "sha256": "d4409b7db44da090f5b9121d65060797bae4324600e49e260f7455dc5627fec0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "423ceb2fc2fbeede59678d4e1d0b2a9d289bb21507e48d145c7687c210ddc830", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "quadrature-and-local-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15667,6 +17680,17 @@ "exists": true, "size_bytes": 32966, "sha256": "d3ff9df18fc2ea5dff58f07b5c8f9ec45aab22300acbb746a569c6c75e64eacc" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "47049680c445618c3f82771bf0ef41d074c98243e4d121d6d8bd89a6a27786eb", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "reduction-or-operator-order-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15751,6 +17775,15 @@ "exists": true, "size_bytes": 275, "sha256": "635323f1a8cbb39026ee3cddbe31d2ac3a93b523da2a1a3f75fab5eef5c5a429" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "635323f1a8cbb39026ee3cddbe31d2ac3a93b523da2a1a3f75fab5eef5c5a429", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -15793,6 +17826,15 @@ "exists": true, "size_bytes": 558, "sha256": "af8a64ab957ff55f9e830563e49f3e41f599a258e00eda8a5a728de6577e108e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "af8a64ab957ff55f9e830563e49f3e41f599a258e00eda8a5a728de6577e108e", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -15835,6 +17877,15 @@ "exists": true, "size_bytes": 1367, "sha256": "fa9db22f3f78192d207b0789a9c04431d6e0716d0a351ea326deae681e4937e2" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "fa9db22f3f78192d207b0789a9c04431d6e0716d0a351ea326deae681e4937e2", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -15877,6 +17928,15 @@ "exists": true, "size_bytes": 196139, "sha256": "cf1174451176c0d3649d93aebd7582cd7cdac243dccc20e6d5553c57c7f39479" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "32c0e763dd456d781403cf370c3e1b0278c84e7d5d926f88740a3d55543c807a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15919,6 +17979,15 @@ "exists": true, "size_bytes": 2742, "sha256": "0b8183b931d26c5d1cc45d3b4ffaa7a8e7886c26339b5918d6d08da5c2dc049a" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0b8183b931d26c5d1cc45d3b4ffaa7a8e7886c26339b5918d6d08da5c2dc049a", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -15961,6 +18030,15 @@ "exists": true, "size_bytes": 10073, "sha256": "6c5ea89f9f3acff0ce6b1c7192a1c787ec7ae6d3f3008e5016e086b5f25d193e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6392c6b17d4a594d10772f69cff18573ae3cee92674ac90a0aaae20e2e468092", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -15981,7 +18059,16 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "d58a1d632dfd9bc8906cefb37c49d3d9097703e70f5b7201186078d9b74355d8", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/io/abaqus/material_property_candidates.h", @@ -16001,7 +18088,16 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "58ec023d66f2c1147b2513aceeee23407b1490b91ceac45d6c74680af0ead34d", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/io/abaqus/material_property_mapper.cpp", @@ -16043,6 +18139,15 @@ "exists": true, "size_bytes": 304, "sha256": "3bb42e635e2de148ce551da3852d734c4eb4f90a32ae93b64ec4bec717a9195d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "f79e8927d15f27b329a2063a8cb33fd856b3196580c914b5bc9208e88f259199", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16085,6 +18190,15 @@ "exists": true, "size_bytes": 556, "sha256": "14c27361cc65667b900099554ecedfdace2b5bac41a4abbbbf4f902839eafe63" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "14c27361cc65667b900099554ecedfdace2b5bac41a4abbbbf4f902839eafe63", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16105,7 +18219,16 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "002ac1d40d6c4d5cc58a223d37ed6462686d6f2701ea6727f91ed133283faacc", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/io/abaqus/step_definition_mapper.cpp", @@ -16147,6 +18270,15 @@ "exists": true, "size_bytes": 286, "sha256": "ed7aea22c28fe87d7496d069c67fbdfcd3bfbac4b07b4c9352d6979fa7f76058" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "6f3fa2b919350a3b20bb3e91bbc17b9399858ad51106b57968b8b4be39ac7517", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16189,6 +18321,15 @@ "exists": true, "size_bytes": 544, "sha256": "2e5da26c092da1640eb1ed8af0381193dc59b71ff1462ca827f01b2abe8842ee" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2e5da26c092da1640eb1ed8af0381193dc59b71ff1462ca827f01b2abe8842ee", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16209,7 +18350,16 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "9414e236d54c3cef74f24c4131db581a3bd6ee6c5d6d61e28a7dc869b4e38e70", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/io/abaqus/surface_definition_mapper.cpp", @@ -16251,6 +18401,15 @@ "exists": true, "size_bytes": 303, "sha256": "b2dd45b490c3cac1284782a5822d0b9b6075ddfc7a6c99d1d7238789fcae058b" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1b28c38e23a923def477fa9fde2f2da159295fbedc84165bab352eb6afaea9fc", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16293,6 +18452,15 @@ "exists": true, "size_bytes": 624, "sha256": "cf83acc9010f8e1fb9b19028717788b141b38b0b91dc1bb0f09b9a10a8cbfd8a" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "cf83acc9010f8e1fb9b19028717788b141b38b0b91dc1bb0f09b9a10a8cbfd8a", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16313,7 +18481,16 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e982851cc3a11fd376ae5e880564aeaf4d7852d0ea5e5ad3596ddaa87866f617", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/io/abaqus/topology_mapper.cpp", @@ -16355,6 +18532,15 @@ "exists": true, "size_bytes": 387, "sha256": "2f786e29243b40abc0c8c341871912853c47ee9173f16acfd8a0e91ce06729ce" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "4de9e8fe635f06e470a3017c48aca4c4b966289837a5a70aff5f200bb24812d7", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16397,6 +18583,15 @@ "exists": true, "size_bytes": 643, "sha256": "2a538902f45a8a2395a1f8b47c08492c75ab3aada13fa7c8e181edff770db830" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2a538902f45a8a2395a1f8b47c08492c75ab3aada13fa7c8e181edff770db830", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16481,6 +18676,15 @@ "exists": true, "size_bytes": 52225, "sha256": "16ad5e122fac30dc1b35d07e3914a0586087efbaccf1248cd3b3f2ca462d37f4" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "8d9361ab7dfcbdb128a707b1c6d51b71bef7252bf8034259788bd9fccd5f7162", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16523,6 +18727,15 @@ "exists": true, "size_bytes": 47295, "sha256": "4406319905f1337c3505b2443633815fba49785824ebee11403d21fabe75071c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a18cb5e4df18f03708d8cda407f669014a2874066d8ec4187f9c9b6c2c8d9b67", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16565,6 +18778,15 @@ "exists": true, "size_bytes": 40575, "sha256": "2f6ca2bd2a39cef0acc5dfc494125a18a4cc03acb1b91cb9a10be6f199b2afa9" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "82e197cc3e59a0d99a282d87cc1cb48173517edc8388064f6839185811752ffd", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16607,6 +18829,15 @@ "exists": true, "size_bytes": 718, "sha256": "46d45f7f576c5ee6f4849e4e0a16fc62f79068c06cb4d2286fd1c4c43c0a2d22" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "46d45f7f576c5ee6f4849e4e0a16fc62f79068c06cb4d2286fd1c4c43c0a2d22", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16649,6 +18880,15 @@ "exists": true, "size_bytes": 2910, "sha256": "811e6839d60b4cfee0f86d01a69b34dd64a9a8cd7457a9d38847271cd548dfcd" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "811e6839d60b4cfee0f86d01a69b34dd64a9a8cd7457a9d38847271cd548dfcd", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16691,6 +18931,15 @@ "exists": true, "size_bytes": 1155, "sha256": "555acd91ad255279e45d89f136b84f4844869459e0d72fc49d2cc4339f0002d1" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "555acd91ad255279e45d89f136b84f4844869459e0d72fc49d2cc4339f0002d1", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16733,6 +18982,15 @@ "exists": true, "size_bytes": 23534, "sha256": "feb777ce0edeeb14b70866174008a4a9dd90f98aa9e398cf1100f3412457485e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2d45539ecfe8516f43ae9f960966646f6b7ebe57910fb2e0c38cbbbca7087343", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16775,6 +19033,15 @@ "exists": true, "size_bytes": 600, "sha256": "df4fe16486427b2a3a17aedb81f5148d61e89b6e7fbea1a1082b69139146c13a" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "df4fe16486427b2a3a17aedb81f5148d61e89b6e7fbea1a1082b69139146c13a", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16817,6 +19084,15 @@ "exists": true, "size_bytes": 12222, "sha256": "8b2a769af447cb29aa41188ec48719249c9168edf5712343a88f14ca96f6d0fb" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "62962671f4d5c013354aa12cfa6c1905276d3645dd5df371523bd5027e724cf7", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16859,6 +19135,15 @@ "exists": true, "size_bytes": 5626, "sha256": "86bfa51fc536267f473aa31c31f09f4f41b8d9b26067bcd3b87d12e09473e4d7" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a48b1a39e70799e02c2a34b9cb8d23aad7622d2b5533ac96b60bd5fa6ebed726", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16901,6 +19186,15 @@ "exists": true, "size_bytes": 2905, "sha256": "30de3d2be1b1b4ac190599525c75ccd1c56c89ff676d044aec22ae50811f1a95" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "30de3d2be1b1b4ac190599525c75ccd1c56c89ff676d044aec22ae50811f1a95", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -16943,6 +19237,15 @@ "exists": true, "size_bytes": 44926, "sha256": "817621b6c60422d09a98ef89e46fd9d223908e5d2fb1076aff767b84f7312599" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "333071ee6d3c4c123327498282d7150f7bfbebfeba1fb3d883604bc99500437a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -16985,6 +19288,15 @@ "exists": true, "size_bytes": 1403, "sha256": "761b78b351e5ae6e47891d42787b3f68790e1aeaaad2c2c7c58be5ad3334509d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "761b78b351e5ae6e47891d42787b3f68790e1aeaaad2c2c7c58be5ad3334509d", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17027,6 +19339,15 @@ "exists": true, "size_bytes": 3174, "sha256": "e5125214002b7a18812016a0af254e50dddf240e72f71c3f6b9a4984ba143c9e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e5125214002b7a18812016a0af254e50dddf240e72f71c3f6b9a4984ba143c9e", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17069,6 +19390,15 @@ "exists": true, "size_bytes": 47968, "sha256": "2b99c5ad7e48fa87b467b4da116dfd1c711a236eeedae2e4484bbe94ad7ad1ac" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "59e0a3bbc9e31782a13bd7262a203e14f5ad0b964a7b863d44f1f2011fa4708a", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17111,6 +19441,15 @@ "exists": true, "size_bytes": 1038, "sha256": "7ec99bfdf9a9d12b98b3a77023268d09ed0a99a872a6ab16378997e36c6089b8" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7ec99bfdf9a9d12b98b3a77023268d09ed0a99a872a6ab16378997e36c6089b8", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17153,6 +19492,15 @@ "exists": true, "size_bytes": 54978, "sha256": "047cf0bb282b9ad9e595fa9dcb12c9b91ad722aed7b746a9e6b7f2a1410241de" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "03031d25e6cee1f68614c69d6388516134ece0addee8153d557670c96bffcbc9", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17195,6 +19543,15 @@ "exists": true, "size_bytes": 38651, "sha256": "b63e029f6d5d1bf6b1f185fd897c09f162f29153f1d6041fbd08c1e62fe38ba7" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "066fb7b105ab7c8e40c0094c15e39aed88531123fbb826797181c5ed20fbba6b", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17237,6 +19594,15 @@ "exists": true, "size_bytes": 10280, "sha256": "f45fd4485b653dfdfe4381e71a0c51834b4589e4f0a7608d8301db7f34f4d8a0" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "f45fd4485b653dfdfe4381e71a0c51834b4589e4f0a7608d8301db7f34f4d8a0", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17279,6 +19645,15 @@ "exists": true, "size_bytes": 2467, "sha256": "5456ea89b83c3575a0eebbee431ce7182bca3beb708ce246a33301763459d89d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "5456ea89b83c3575a0eebbee431ce7182bca3beb708ce246a33301763459d89d", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17321,6 +19696,15 @@ "exists": true, "size_bytes": 21433, "sha256": "aa748145336164ee42e8920e52061afead9d4fdfe73369835693ba2485acf6d3" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "aa748145336164ee42e8920e52061afead9d4fdfe73369835693ba2485acf6d3", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17363,6 +19747,15 @@ "exists": true, "size_bytes": 27425, "sha256": "961c81bf79174acafe1a239e0859df025bcd4d9a2d4412b8abc5de4f67eb2e7e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1645a88ca476a61593dba4222bf773e8ff418669a919d7c15c393de84a7ccb38", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17405,6 +19798,15 @@ "exists": true, "size_bytes": 2182, "sha256": "1c7483f34380dc9432d9d3a917402384f6f92dbb79752cdd610b3b5c29408be4" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1c7483f34380dc9432d9d3a917402384f6f92dbb79752cdd610b3b5c29408be4", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17489,6 +19891,15 @@ "exists": true, "size_bytes": 6477, "sha256": "af82862af945227f2c5b37018b1a4f2d34c02ae96b01e5f016b3bfcb7c125850" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "af82862af945227f2c5b37018b1a4f2d34c02ae96b01e5f016b3bfcb7c125850", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17531,6 +19942,15 @@ "exists": true, "size_bytes": 13459, "sha256": "60f1abeaee269943e27463705d96db412b3d418ea535853d9a8bcc7cc9d70887" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "cc49655f10bcc231fee876b642d317b844a078d8fd07a5480fb855ea0dfbb729", + "changed_from_integration_baseline": true, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17615,6 +20035,15 @@ "exists": true, "size_bytes": 3252, "sha256": "350a78f7f7ab11265a6492a8e0bc0651383972c0426b81408f2cc545f143be47" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "350a78f7f7ab11265a6492a8e0bc0651383972c0426b81408f2cc545f143be47", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17657,6 +20086,15 @@ "exists": true, "size_bytes": 2731, "sha256": "e9f4a15c83ae983b25bc45554ad376fdb4208a803b3068a564dbb3bf0d9f2056" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e9f4a15c83ae983b25bc45554ad376fdb4208a803b3068a564dbb3bf0d9f2056", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -17719,7 +20157,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "d5b4d2f8de956f95c9a3a3361b0aa806334569f7057a5a2d31047299d66fdf95", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/math/dense_blas_internal.cpp", @@ -17761,6 +20210,17 @@ "exists": true, "size_bytes": 7769, "sha256": "65eb751e0316d06311ebb899f09e66344fb5cd9637460f5b803c4d3c2b24e54b" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "4c34c393d50fe11c23fdf562487518713ac743c99eeec8b29a1d387df9568025", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17803,6 +20263,17 @@ "exists": true, "size_bytes": 5192, "sha256": "cf45c1836fd5481ac827227db9b171deeae380c67f8290623121c8ea37e15891" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "15008a79758189131893f983bfdae0370beb5e8cf2fb05b891a51c40c3b579d6", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17823,7 +20294,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "06fb743582dfbb1a114a443b20b6abe86a9123bb74f85e71bc784f2bb71f470d", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/math/high_low_arithmetic.h", @@ -17865,6 +20347,17 @@ "exists": true, "size_bytes": 9016, "sha256": "4f360fd4d11fca68863e9fa7c084ed80e70fc4d903b9df170ac946d19472987c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "bf26c811169c71cd4aa647e2eaa18a8605ec2fec9a2b46b66fa6b591d2767045", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17885,7 +20378,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "8f1f0f731928dc1951007802aad56a00e3ec349e9cdb8eb02e0321b449c03f2b", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/math/jet_linear_solve.h", @@ -17905,7 +20409,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1d3e0b93bef3eb88558489443a2ab9f072dee39675117e37f7884a7dc0109910", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/math/matrix.cpp", @@ -17947,6 +20462,17 @@ "exists": true, "size_bytes": 5059, "sha256": "2d10bedd159a06c25225685c5504db2b075b02379653d2bbf49b31278e66a611" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "b26c9ae6c7d7abb002d77314f7bf3c1c4bdfcb7c7e54a1abce57734b73fd55fb", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -17989,6 +20515,17 @@ "exists": true, "size_bytes": 10857, "sha256": "36b6d58ef1a1bcbc525e6483a887fe04eefbc19e8e429fdac25d0b7b74dc920d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7c0b95cb26e78f9634e35ecbda098c47dda3e7df5755d1c99bbfbaf4d0353546", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Removed unused dof_manager include; no arithmetic diff observed.", + "numerical_equivalence": "not-claimed" } }, { @@ -18031,6 +20568,17 @@ "exists": true, "size_bytes": 2741, "sha256": "17f557a916f569e4ad82084cad6fa281bec950cc1b6ca87d37558c1223a396cf" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "71834bc656340615dab152c7a3222f78e4996f8560964e368b435246f47491da", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18051,7 +20599,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "e7c562232318be95281a226a388b014669f8beb580b238fc47b507095d8767b1", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/model/AGENTS.md", @@ -18135,6 +20694,15 @@ "exists": true, "size_bytes": 33724, "sha256": "1aaa071bc1d9ed1a6feb50b9341cccd6e9013ee9cace587dc7367106795b4d75" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "1aaa071bc1d9ed1a6feb50b9341cccd6e9013ee9cace587dc7367106795b4d75", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18177,6 +20745,17 @@ "exists": true, "size_bytes": 19543, "sha256": "50fade5b588b9da1e56fe012e697d52db0f4808298783177fd43c4d6021dbc6c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "07a7a7ff6d2f79fe8c096a6985759bfa4515d2dfe1adc36dd7fc049bdd2ed041", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18219,6 +20798,17 @@ "exists": true, "size_bytes": 8250, "sha256": "852bd2969300094184ad9f39bce0d0b0b974987c2fb2134feb31b77a17d7f50d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "251a981e4f5c528c6de98694bc7b39c6a444ab4e9d6d176cf7a91c1f24fb55db", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18303,6 +20893,15 @@ "exists": true, "size_bytes": 5427, "sha256": "2f74db860abb18223b1113ff27c54445668145b2ae990183922b595c455acf7b" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2f74db860abb18223b1113ff27c54445668145b2ae990183922b595c455acf7b", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18345,6 +20944,15 @@ "exists": true, "size_bytes": 2827, "sha256": "f25e6ce77781a03644b1489d3dc6a01af7b5dfae2e9b58c02fa090026839d6b9" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "f25e6ce77781a03644b1489d3dc6a01af7b5dfae2e9b58c02fa090026839d6b9", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18429,6 +21037,15 @@ "exists": true, "size_bytes": 4039, "sha256": "162e775a6ff316790f4906568e7c520e8dfc5d441ccb054a14a645ec63b78bd9" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "162e775a6ff316790f4906568e7c520e8dfc5d441ccb054a14a645ec63b78bd9", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18471,6 +21088,15 @@ "exists": true, "size_bytes": 543, "sha256": "a76675ad61681d1b4435e0512f913a9be146fdde9b1ea22dac13e658fbb313f6" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a76675ad61681d1b4435e0512f913a9be146fdde9b1ea22dac13e658fbb313f6", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18513,6 +21139,15 @@ "exists": true, "size_bytes": 4690, "sha256": "3f3758f6618053ced0abef12b523949fa89e4c53059686debd12534adc3b4c04" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "3f3758f6618053ced0abef12b523949fa89e4c53059686debd12534adc3b4c04", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18555,6 +21190,15 @@ "exists": true, "size_bytes": 845, "sha256": "4e9ec0b48f6c0499697e9006032bb7f6c0535007b7732f9d76ba3536328f2f48" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "4e9ec0b48f6c0499697e9006032bb7f6c0535007b7732f9d76ba3536328f2f48", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18597,6 +21241,15 @@ "exists": true, "size_bytes": 12166, "sha256": "859313f28b759f04e607826babd53e0f9be053224b95d337c9ff33f3e80bcfb6" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "859313f28b759f04e607826babd53e0f9be053224b95d337c9ff33f3e80bcfb6", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18639,6 +21292,15 @@ "exists": true, "size_bytes": 1480, "sha256": "3501eecba97e84852901c44a63c23e04f1a44cb29ead4940c9090c78c4fab53e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "3501eecba97e84852901c44a63c23e04f1a44cb29ead4940c9090c78c4fab53e", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -18659,7 +21321,18 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "b362e5ade74f5dd86e2366b96a7c6863b8a742f5f114454274989c45e629431c", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/results/recovery_candidate.cpp", @@ -18701,6 +21374,17 @@ "exists": true, "size_bytes": 1284, "sha256": "4a1558c3d6c719ef55dbe06374d07d8013b87908c2acfc563af748d9b26ad96e" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "af82c9b07d6cc2e48efafc6d1c09e3a9c5f7199aa8246e434415dd08d951d19e", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18743,6 +21427,17 @@ "exists": true, "size_bytes": 2281, "sha256": "52601ac6bc57a8f57d064d491e38f538bdd0e278aba3e09dfc52e9ea18ca1d17" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "753826e7d5bcec2af42dece8f8b61d21a9dfd2e6c192d5b4cd9b345820747db9", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18785,6 +21480,17 @@ "exists": true, "size_bytes": 27290, "sha256": "880911864fc221ef6c36d7aa86b1e6435bd01c65fcc4a2922acbdf4bf688c73c" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "7866b65949059314cb63948c3977c04f78e301bbd5ee0985990a73963f43ac12", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18827,6 +21533,17 @@ "exists": true, "size_bytes": 2377, "sha256": "55aea88441735deb433ff93cffb9bdd06c906d11d400e7852d8307429df5558d" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "f86eff1d4f7edb3ccc41bef084723442db53871eed1ace926a88298ad5770209", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18869,6 +21586,17 @@ "exists": true, "size_bytes": 755, "sha256": "34ade52c40572b90318e68585e91f46b3e121952343b4b0d0180dc419d3686d9" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "0f48bff1b0f7659ed85b7a17a0e3e4b3a5f08bf97de38480ae8ce78566dd8bc4", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18953,6 +21681,18 @@ "exists": true, "size_bytes": 56309, "sha256": "bc09f19c7737efb515cb9521d31bc506aa1ef95bd96cbbe27d1572397abf5f86" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "2a504cc3dc4bd7db44c47ae0e538f29a7650382062e74b3474447a9b0b0bf682", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -18995,6 +21735,15 @@ "exists": true, "size_bytes": 2069, "sha256": "42ad3e07300fda75f73ba64187f6c3630339ca0c7abebf284f641ed0f44cafbc" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "42ad3e07300fda75f73ba64187f6c3630339ca0c7abebf284f641ed0f44cafbc", + "changed_from_integration_baseline": false, + "numerical_review_categories": [], + "review_owner": null, + "classification_basis": "Identical to T1 integration baseline; no T2 byte change.", + "numerical_equivalence": "not-claimed" } }, { @@ -19079,6 +21828,18 @@ "exists": true, "size_bytes": 12616, "sha256": "f7b0de5c715661953ce66b0bfde719a482e08d10c2e39a0dbff66378409e5341" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "9e15164e8a726dbfb58cdd71a202477b2b529e0928be46f68eb04f0df83b94bd", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -19121,6 +21882,18 @@ "exists": true, "size_bytes": 10285, "sha256": "c278a1d8d7333b464c0d0e9883fa446f57c86c183754c913e2c651ff8bfb4168" + }, + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "da98030db0fa6377a477a58651184205a85039dbfef1aae6f8d73a62de2ae799", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" } }, { @@ -19141,7 +21914,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "a039127e896a4354c2c580aa28657d01c3ee4fbfd32634625f87f2ce0e4eaeba", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/solvers/linear/pardiso_session.h", @@ -19161,7 +21946,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "afea0500b8519292469aa2e51eecb57070df08d039c4cd9d5cf7a6086b382180", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "src/fesa/solvers/linear/pardiso_session_test_seam.h", @@ -19181,7 +21978,19 @@ }, "working_tree_equals_blob": false }, - "policy": "adopt-refactor-bytes" + "policy": "adopt-refactor-bytes", + "t2": { + "adoption": "copied-refactor-working-tree-bytes", + "sha256_verified": "4fdd30e28205d6362de2dd9ea43d7f354384b700afb13bab96e700c0903851c3", + "changed_from_integration_baseline": true, + "numerical_review_categories": [ + "formula-or-arithmetic-execution-review", + "cache-trial-commit-lifetime-review" + ], + "review_owner": "T5", + "classification_basis": "Conservative file-owner triage of changed API/implementation; independent diff review pending.", + "numerical_equivalence": "not-claimed" + } }, { "target_path": "tests/AGENTS.md", @@ -19265,6 +22074,13 @@ "exists": true, "size_bytes": 9773, "sha256": "7abaa48fa059aac076957434478f35eb9ff9afa3970ddc7d15648cb43b4eff32" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19307,6 +22123,13 @@ "exists": true, "size_bytes": 11520, "sha256": "73da42092ea16d533f8a1725f6e925338fb4d8ff6901c5a462004aef3cacc770" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19349,6 +22172,13 @@ "exists": true, "size_bytes": 17284, "sha256": "c796815b0cdc8b655da47ddab48bb3df2ef5d7334d949c9ede355e046b12294b" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19391,6 +22221,13 @@ "exists": true, "size_bytes": 11907, "sha256": "1e061b20491b18339e35610d48168180a3a8dc329fd09565e392268dbce7011c" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19433,6 +22270,13 @@ "exists": true, "size_bytes": 9266, "sha256": "f160a13c739276bb27b6b769f4b5d7fc53ab6bbe0e4917b1388aa171313f834e" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19475,6 +22319,13 @@ "exists": true, "size_bytes": 15403, "sha256": "76f0d621c519f1ca1cb68fafd700bca52c94c69ac975a274eac995298ff77df3" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19517,6 +22368,13 @@ "exists": true, "size_bytes": 3289, "sha256": "d788dbffcaedabfd16337b9683fd30803efb69cee2e64da08c09c4ccff29d12f" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19559,6 +22417,13 @@ "exists": true, "size_bytes": 11007, "sha256": "9c7d218fb43f93b77f0b84d158fcdc32033931a63f67b46e0e8adbac48b678dd" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19601,6 +22466,13 @@ "exists": true, "size_bytes": 53017, "sha256": "76b826d9162c196c136134c48c4c5116b8b95d0dee90f65fa7a8359f97d26894" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19643,6 +22515,13 @@ "exists": true, "size_bytes": 19382, "sha256": "2d4d4f58df2a9f08b25f850c59617100f2a8af10ba7bdebb7ee80e15b079ad27" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19685,6 +22564,13 @@ "exists": true, "size_bytes": 3041, "sha256": "7632397db2daf06eb0ff79a311f4f61832f415ebee4aebe334c8bf5d97e83772" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19727,6 +22613,13 @@ "exists": true, "size_bytes": 19477, "sha256": "07d63eb93467254f069d6ae86e2af022287e1ff0dba8272dbb08e830709a9ff0" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19769,6 +22662,13 @@ "exists": true, "size_bytes": 5981, "sha256": "369cc07b69791c67f80fefdfebd3b3986bdd7ec3ddae0fc20cc7f9f01f1e4d07" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19811,6 +22711,13 @@ "exists": true, "size_bytes": 7269, "sha256": "4fe3fa020951bd776343af5d26def36392794dd74c5e19ca3d57427c489c6831" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19853,6 +22760,13 @@ "exists": true, "size_bytes": 19864, "sha256": "671f8895de95c3883587d1766671fc911af679e0ee1104ba90d6f260e2bb7ed4" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19895,6 +22809,13 @@ "exists": true, "size_bytes": 10910, "sha256": "8de80a54713f644fb3f7e0d4a1f2d633cf05973b3fcb71ad23bb9e3b318b5296" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19937,6 +22858,13 @@ "exists": true, "size_bytes": 2628, "sha256": "f91504b627d58c1ea91174e4d32575067afd8ad6f6e8e9ca79b0bce519a4e661" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -19979,6 +22907,13 @@ "exists": true, "size_bytes": 18536, "sha256": "b46d4b18fcbf3bbcba3547c41cdecc3f206eb6ed6d2df44b7de4f3a8c2c86000" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20021,6 +22956,13 @@ "exists": true, "size_bytes": 1378, "sha256": "96423b9a6c85e87752a28c38eed575c834c724caad6692122523f49888391a15" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20063,6 +23005,13 @@ "exists": true, "size_bytes": 8293, "sha256": "f71ba9da827ff4740ce536149fef0f03dcd226f6c61f72a721973cae2b159e2a" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20105,6 +23054,13 @@ "exists": true, "size_bytes": 8251, "sha256": "60c30a9f9b735df4571ef7bc262710fc3c1e84fb98b86b2d25611ff1406cb745" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20147,6 +23103,13 @@ "exists": true, "size_bytes": 39115, "sha256": "1e23f52e7a9ff7172bf67f5f9c3e460a05b86089b4df1f91566e502b3f7a3c69" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20189,6 +23152,13 @@ "exists": true, "size_bytes": 2370, "sha256": "51c8b78b44583986eeb175a839fd72a11ca5f3405bf016432da9563878c8f1f6" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20231,6 +23201,13 @@ "exists": true, "size_bytes": 31087, "sha256": "b886b3cf9a6d3569e73f6792ce8ff0a84052c81eb21d78a31245fc74033bf370" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20273,6 +23250,13 @@ "exists": true, "size_bytes": 17383, "sha256": "d50d0cc07497cb23b71c50e59b5eba6896d601f76c57a416cb5d8fe2f950b269" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20315,6 +23299,13 @@ "exists": true, "size_bytes": 75202, "sha256": "e71f156cb5bede1369b6b2e0999d21c5e89e8af5516f74ced906811a3eca55f5" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20357,6 +23348,13 @@ "exists": true, "size_bytes": 4718, "sha256": "6f3832cec9b2ab5770948480045b2e454a5d0bc8fd2b6c38f0e134fbaf2983be" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20399,6 +23397,13 @@ "exists": true, "size_bytes": 62797, "sha256": "9680d6dd2deef156b5c8d000167cd6d3f6e29fe1b1fecc5a0f9cd6ebc0fa1602" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20441,6 +23446,13 @@ "exists": true, "size_bytes": 3060, "sha256": "1ca9383ffed095fdb372fe7d16ec3d9c6fbace46c7a103e94c27220fc99b56b0" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20483,6 +23495,13 @@ "exists": true, "size_bytes": 31014, "sha256": "e39732a6cb8747725a43f7fc6341fd4688d04cd3f9f936ec33cbc79b232886af" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20525,6 +23544,13 @@ "exists": true, "size_bytes": 5369, "sha256": "b2e6065df0d673be20fb10fbf0ef58705fc04c1788b5e52d1faaf7ebda33274f" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20567,6 +23593,13 @@ "exists": true, "size_bytes": 2125, "sha256": "9918e9b43e9b777dae620fc552badccc5570bb246a6499756df83b1c207f9e24" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20609,6 +23642,13 @@ "exists": true, "size_bytes": 6933, "sha256": "be4a8896f4847dec26c07578c91d97f23f132c8a97c2507e5614011f70aa600f" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20651,6 +23691,13 @@ "exists": true, "size_bytes": 1167, "sha256": "45a004e6e7ab3dba315796d54e1347b54fca3e1cad3b637a2dda6884ab846d7d" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20693,6 +23740,13 @@ "exists": true, "size_bytes": 2901, "sha256": "f796b0d84b1289f58caa791fe2c380e1b28b4322030ed505476f39c788f0db86" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20735,6 +23789,13 @@ "exists": true, "size_bytes": 18518, "sha256": "0cb4c1b8595a16219808d6fe661571b59f7b7f2e100749d83baf2506dc0a61a0" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20777,6 +23838,13 @@ "exists": true, "size_bytes": 1961, "sha256": "c0e30c7bdd6ef3fad7e0d69159888a15e35a096adbd77c8481913063d703a9e0" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20819,6 +23887,13 @@ "exists": true, "size_bytes": 13487, "sha256": "d98e4228c027b4cedd45cc97c75fbd56694f91ecff32c875e32da384faa9a19e" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20839,7 +23914,14 @@ }, "working_tree_equals_blob": false }, - "policy": "import-refactor" + "policy": "import-refactor", + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": null, + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" + } }, { "target_path": "tests/test_compare_hht_dynamic.py", @@ -20859,7 +23941,14 @@ }, "working_tree_equals_blob": false }, - "policy": "import-refactor" + "policy": "import-refactor", + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": null, + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" + } }, { "target_path": "tests/test_cpp_policy_contract.py", @@ -20926,6 +24015,13 @@ "exists": true, "size_bytes": 8939, "sha256": "008c38d245f54595a9a057575e264b408f235df5e9ff4fad3ef342ab6ff4301a" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -20968,6 +24064,13 @@ "exists": true, "size_bytes": 6541, "sha256": "fc1756cf35f167a555753a2b8ae5c3be3dec48e851ffb2d1a02003088024dbb1" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21010,6 +24113,13 @@ "exists": true, "size_bytes": 2138, "sha256": "5ee75fcbd1bc5a9a634d16239720a814a8fee06b04acb0893a617d557fe8cda7" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21052,6 +24162,13 @@ "exists": true, "size_bytes": 13967, "sha256": "25e30b540c37be5c7699132ddf7c5da319a4a1bf3fb8988b054c8e9941ca594f" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21094,6 +24211,13 @@ "exists": true, "size_bytes": 6308, "sha256": "8489602024286b7c66c26750ecbf627d361b893fa9083c7d2dd97d34bbe3c25e" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21136,6 +24260,13 @@ "exists": true, "size_bytes": 9389, "sha256": "8b9ae7711b40d67884aa28e863e458d968ae4d5c717e00034405aced6bddc9bb" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21178,6 +24309,13 @@ "exists": true, "size_bytes": 29938, "sha256": "e926831b44741880a7a729c57266c668eeeccc9130be07eacdb97c5330338ad2" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21220,6 +24358,13 @@ "exists": true, "size_bytes": 27308, "sha256": "7b16fff8a3edd6482b56b0c6ac02cbac5b292aecb05c82d51ac460f691befd3a" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21262,6 +24407,13 @@ "exists": true, "size_bytes": 9021, "sha256": "9d0c672737a65b301b38b949aef26d0099451719a7fd98ffa7c5d1171e97a885" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21304,6 +24456,13 @@ "exists": true, "size_bytes": 84430, "sha256": "d715219a60c73ce9803f2c386be3843221c48b1f90882c224ad714db3ea6c785" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21346,6 +24505,13 @@ "exists": true, "size_bytes": 184096, "sha256": "cd99f3300d17e9e74297ddfd12c969fdc10c58bde027ccbbe25b1d39f4d29c6a" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21388,6 +24554,13 @@ "exists": true, "size_bytes": 21087, "sha256": "03565773d95b6bad350d360bb1b2b2489c81b19c5877a5c7bcc6362c951fb013" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21430,6 +24603,13 @@ "exists": true, "size_bytes": 3797, "sha256": "068a7d057ec10b7f70a883d56ad54bb12f20873028b3bd7c34a1eb5c29adce20" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21472,6 +24652,13 @@ "exists": true, "size_bytes": 25068, "sha256": "d3101c094646165ee241aa223245bcbf7d58accb1a8f018d2c83fed3f484b9dc" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21514,6 +24701,13 @@ "exists": true, "size_bytes": 19401, "sha256": "71de713d652861fd086471944762c2ce289442e6e2ba2e259db76fad20e63db1" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21556,6 +24750,13 @@ "exists": true, "size_bytes": 23176, "sha256": "d12853400861a54e112f715d41be7470bf32caff38740282301f452c636a668c" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21598,6 +24799,13 @@ "exists": true, "size_bytes": 13806, "sha256": "afb4cc6cb6b1b6913bcfd0d473260ab060c91b66835e5570073472e3bc7f3dd5" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21640,6 +24848,13 @@ "exists": true, "size_bytes": 69044, "sha256": "71a4ef2edd5b2e4cf40f47c17468a6f37cd7ca8937df212436438cfa65dbba70" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21682,6 +24897,13 @@ "exists": true, "size_bytes": 32579, "sha256": "2973549ac31485ce1ab52ad5dc69ca0e8790f437361cf696f53e4bac0b9c627c" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21724,6 +24946,13 @@ "exists": true, "size_bytes": 2127, "sha256": "2ffd1ec89405f926f038cb58fc612604ff8e17853c134caf7cb127c32685cefd" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21766,6 +24995,13 @@ "exists": true, "size_bytes": 37729, "sha256": "34391f18bae6f8ff3abe2d99d252ab74cbdd481fd066ba1594419eb2f1da5ab1" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21808,6 +25044,13 @@ "exists": true, "size_bytes": 2523, "sha256": "1693abc34bf1b4346ff3a17979cb8a763cb16950fee6b5a5471e402d4faf621d" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21850,6 +25093,13 @@ "exists": true, "size_bytes": 13289, "sha256": "a6d89b30f8bb178f16912e6f1fca3bed6b31c6b4b3439095542d51ae5eef5b60" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21892,6 +25142,13 @@ "exists": true, "size_bytes": 4609, "sha256": "a2f5f4ff554e4c7d127640394d45cd41bba3df2adde92d39071f720af402ff3e" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21934,6 +25191,13 @@ "exists": true, "size_bytes": 21180, "sha256": "ee408d0deb583f35c838e9e7457ef28de8d017edab31e6c2eae3b6eb3cdf03cd" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -21976,6 +25240,13 @@ "exists": true, "size_bytes": 5330, "sha256": "23aa6fa1dac950505061d2d64a2ad10abffb76bcabc0232cd0afef512c863107" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22018,6 +25289,13 @@ "exists": true, "size_bytes": 23351, "sha256": "34eb602efea7903941426ad18f10ba54bdabd46e4e262ba6efcb5ffabf2af2d3" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22060,6 +25338,13 @@ "exists": true, "size_bytes": 4221, "sha256": "c33eb3d92d7e4fd8c3ae369e223cc123437ab59d17653a4114fc4d80b1230314" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22102,6 +25387,13 @@ "exists": true, "size_bytes": 17733, "sha256": "189583b8d3501b8337b55f56051ea1d57e55626bb5358df9787651ef96c34697" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22144,6 +25436,13 @@ "exists": true, "size_bytes": 7775, "sha256": "65b0c1bd703fc7bafdbd8200cf61935a039da6b80295f0ddee35027d2cf2dc78" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22186,6 +25485,13 @@ "exists": true, "size_bytes": 14940, "sha256": "bcac60bb3167ad3e6ec2d45f9ee08c7535bb121f71cae21a7cac9053440ec25a" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22228,6 +25534,13 @@ "exists": true, "size_bytes": 804, "sha256": "8bf9bfef45c45c9a09e0a0f98ee4cc38316e2550894c6e27ca599fdd650298b8" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22270,6 +25583,13 @@ "exists": true, "size_bytes": 8705, "sha256": "b5ff07b5925aab743568eff705c8827f7186889ad15541e90d2ed9fe6998ce79" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22312,6 +25632,13 @@ "exists": true, "size_bytes": 13149, "sha256": "ba9c09cde54c8d7ff5a12a38159a7c1de0001262a0186201a81bcbd2bade63d6" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22354,6 +25681,13 @@ "exists": true, "size_bytes": 2224, "sha256": "b26eaed43661b405fda69e4a287f40ce7f0d5efd2f7ff7aa0459cf32dd802ae4" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22396,6 +25730,13 @@ "exists": true, "size_bytes": 2159, "sha256": "440becb7f8e8ffe6db9b32b757b08a56baea19da46808dc42c426e0f9edb14e5" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22438,6 +25779,13 @@ "exists": true, "size_bytes": 680, "sha256": "601d563a839e0ce63c6a47ddbb4cb2dc60386ecb2483519532e969affe387b85" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22480,6 +25828,13 @@ "exists": true, "size_bytes": 2290, "sha256": "5c035458f1d67d43c340b41fa3f4581a1b3b1df2859903ddfa12da838ac7ff69" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22500,7 +25855,14 @@ }, "working_tree_equals_blob": false }, - "policy": "import-refactor" + "policy": "import-refactor", + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": null, + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" + } }, { "target_path": "tests/unit/elements/element_factory_test.cpp", @@ -22542,6 +25904,13 @@ "exists": true, "size_bytes": 18723, "sha256": "39cdff0a92b9a86f0efe54457c47f57bb8fa0a48887eae940198046c8f5249f0" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22584,6 +25953,13 @@ "exists": true, "size_bytes": 48466, "sha256": "36b1305028805eacc62bf2e85b8abdf3512eb322f1a41429062b810365d79c0f" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22626,6 +26002,13 @@ "exists": true, "size_bytes": 8810, "sha256": "6404d4c360ec2cc2779d394442b7ed9dd520ad9a2742394650f50a699619c5af" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22668,6 +26051,13 @@ "exists": true, "size_bytes": 54589, "sha256": "e346ef7775cc96410eacfea1e7e15ecf2b62b6f125a2e475f9e15e3e97e1acaa" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22710,6 +26100,13 @@ "exists": true, "size_bytes": 59984, "sha256": "893b48807ff3d9b7cb80069df3cca46787af5dfc366f244c726f4bcadf59b451" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22752,6 +26149,13 @@ "exists": true, "size_bytes": 20671, "sha256": "1fd73892c2678a5b3ad8fd34edd0292394836a3494c540f4ecc005b6e6a269d9" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22794,6 +26198,13 @@ "exists": true, "size_bytes": 52283, "sha256": "690b9a24e116e86b67d26c75d76305c766aeaf2835fb1c040ba1cae247734f31" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22836,6 +26247,13 @@ "exists": true, "size_bytes": 56993, "sha256": "eb4562c8abedcfbc7c2f953b58bae30f67a84d7ccf0bbea9ae384dfcf595f055" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22878,6 +26296,13 @@ "exists": true, "size_bytes": 35413, "sha256": "a2053f0d24af51ecdfe345ef14267b8a57a2530e994c46da68a5bb17f7250341" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22920,6 +26345,13 @@ "exists": true, "size_bytes": 11552, "sha256": "0436d99db81007fd4713cbcfd2850b351089fc3c05ab34e291a90e1e7621d07d" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -22962,6 +26394,13 @@ "exists": true, "size_bytes": 21758, "sha256": "70b2d6efab8783f723e6abf838fd6c50a1ea25a436480271f0ad9d9234b062db" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23004,6 +26443,13 @@ "exists": true, "size_bytes": 27402, "sha256": "752d1a2138fc77ec40346e6575f87c9e96d69dd9f9c097eff316e6de887d2195" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23046,6 +26492,13 @@ "exists": true, "size_bytes": 47762, "sha256": "cdc14b4c935734140f44849672a8a15b2137233b1a5853a685577d3eb16dc728" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23088,6 +26541,13 @@ "exists": true, "size_bytes": 8766, "sha256": "a515704fe97d521d11a2b7c95e41e0f7ec6557f3ee7e4dd5017a8c9757358f9a" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23130,6 +26590,13 @@ "exists": true, "size_bytes": 7469, "sha256": "092b4127f43e6ba27485ad4043ba8b2d80b310299cdf66858fbc239342faa746" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23172,6 +26639,13 @@ "exists": true, "size_bytes": 26849, "sha256": "0804a9ef5cb15fdee5c06d4f3ba05b626ebcae6952360bb769271d457bc9530c" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23214,6 +26688,13 @@ "exists": true, "size_bytes": 6929, "sha256": "e5a7cec78d6b44cfd120f3883d44964842e52ab1781c6a9c678b93c1768a893b" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23256,6 +26737,13 @@ "exists": true, "size_bytes": 5986, "sha256": "30c6b51c5910fb07f94969370ced1e063bcbf212266e228e3c09ef5a4575ad96" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23298,6 +26786,13 @@ "exists": true, "size_bytes": 3089, "sha256": "6a0ecd5814f476c566f96a1e64d8e1b2757a0dfd70f17d3543519b1d6226a7b7" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23340,6 +26835,13 @@ "exists": true, "size_bytes": 24047, "sha256": "f28acc507f7b42094ff105589aa020384ae0c3b6c6b4c67edb7d4e36f7928af6" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23382,6 +26884,13 @@ "exists": true, "size_bytes": 47468, "sha256": "efd4b1c00fa1db1fa4fe9ccd369c34f9bea4cc2665b5c00972792a0fd66d9483" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23424,6 +26933,13 @@ "exists": true, "size_bytes": 30528, "sha256": "55a2e65c40382b393210d36625666ce014008eaf3cbfbe9610a6dfaf0f840d11" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23466,6 +26982,13 @@ "exists": true, "size_bytes": 7003, "sha256": "249f07429c5bb0dd50db784f6be559e15db7c705baa3db44e0591c9c7914d9e3" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23508,6 +27031,13 @@ "exists": true, "size_bytes": 59788, "sha256": "dd680025af1f1e85cfd08e97d2dad7db8c857238d3b73cb23d824690670f58c3" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23550,6 +27080,13 @@ "exists": true, "size_bytes": 7893, "sha256": "cedde4c3b4576ff6568b8dabefbc0ad56bc54ef4d3adbf2c8eb74ad7f7f711e4" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23592,6 +27129,13 @@ "exists": true, "size_bytes": 24705, "sha256": "ea65dd1a637d3517aba6239dd53b411bf465a04769ae57ffc2ca970247080a4a" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23634,6 +27178,13 @@ "exists": true, "size_bytes": 40171, "sha256": "bde4a8151895a48cd9d3bc6216b5d97ccfb51cc9991fd13c9eac5c30f4ad8c7e" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23676,6 +27227,13 @@ "exists": true, "size_bytes": 2362, "sha256": "f55daaac80ffbb33f56431eb37931b48dc0202107b74e298254f9fcd2a2e5977" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23718,6 +27276,13 @@ "exists": true, "size_bytes": 41517, "sha256": "576b253a222e41e7b8bd6643a47706eec1ed333eaf5a09c2206b89ef1bb06895" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23760,6 +27325,13 @@ "exists": true, "size_bytes": 4255, "sha256": "ce8c7aa7d0372bf0cd2f1627ed19cc2b1c603240d03517e959169eb746f8dd64" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23802,6 +27374,13 @@ "exists": true, "size_bytes": 20557, "sha256": "094f2106b557380c45c24cf92d2f4b860ece37d53faea45b704bfd456be26945" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23844,6 +27423,13 @@ "exists": true, "size_bytes": 2435, "sha256": "0e4f0a0beafc60dfc9ed3a016a544ed2b93a305867f8b820dc6817ebe58d8b7d" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23886,6 +27472,13 @@ "exists": true, "size_bytes": 11958, "sha256": "d63714753d2cb3dc08d74b1d95b437b2a44ecf84087888f8814d62f227623aeb" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23953,6 +27546,13 @@ "exists": true, "size_bytes": 10425, "sha256": "bb3a73827650ecd71ab92015e1add5c2836c5e9bc6e712068823e228d7389f5f" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -23995,6 +27595,13 @@ "exists": true, "size_bytes": 4528, "sha256": "24d515a2397f12e2951d055c69e82e00f1cef93c2451520a5d180cc8ca02ccf0" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24037,6 +27644,13 @@ "exists": true, "size_bytes": 9774, "sha256": "ce2d95faaa89e5b4887d791745eee4b97e882482fa91e04b787dceaa268d663b" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24079,6 +27693,13 @@ "exists": true, "size_bytes": 3781, "sha256": "73d76da9064531db15cd5c7aaaafca04cffcc9853b56171055ed35789cf649ae" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24121,6 +27742,13 @@ "exists": true, "size_bytes": 2518, "sha256": "4b7cd27e186d0cc818854d87e9540d182e952dfc2c3a372f67a656f4d4d4b873" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24163,6 +27791,13 @@ "exists": true, "size_bytes": 17115, "sha256": "fc0dd95c444cac746db6083b88376bc8365ccd7d26790dd0dd6d51c3f41d6f77" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24205,6 +27840,13 @@ "exists": true, "size_bytes": 7247, "sha256": "8e73c0944e4a0592a63c397fe368b2568b35f9b176af5cb8a5f7eb409dd4556c" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24247,6 +27889,13 @@ "exists": true, "size_bytes": 12376, "sha256": "9fee2fde5365fea4144fc87591e88e48dfd45376f3dd0ead45f9b7246fff1b3f" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24289,6 +27938,13 @@ "exists": true, "size_bytes": 5863, "sha256": "0fd2a3f57be9e3dcc6a0710f23d07618b100d9f8fa6d46945bf7951063145ebb" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24331,6 +27987,13 @@ "exists": true, "size_bytes": 12468, "sha256": "e35fdae0a9d314a157526b4ebd72ff54eef3f8123b3646fd8f5f3c50b39bbaea" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24373,6 +28036,13 @@ "exists": true, "size_bytes": 8706, "sha256": "754d05288c9ba4de3c7cba2153621be4af8489b5c7e0946e387ee86ffaa0776c" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24415,6 +28085,13 @@ "exists": true, "size_bytes": 4238, "sha256": "37fe800c84baee1240c919337d93fdaf0b06540901fcf27c1de88b3cab07b049" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24457,6 +28134,13 @@ "exists": true, "size_bytes": 13625, "sha256": "ee0513e6ac8e92851a82d5f5bc932f841483b2640413b105465b71bb77cb1ccb" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24499,6 +28183,13 @@ "exists": true, "size_bytes": 30343, "sha256": "e5f882061497e99d8210a474c7f662c17de029fc7542ae8b4c7334a2be162c6d" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24541,6 +28232,13 @@ "exists": true, "size_bytes": 55269, "sha256": "7b9358cc8459e3ec9c7fd6384982951ff5520dca7ee1b41eb671976c79c254e6" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24583,6 +28281,13 @@ "exists": true, "size_bytes": 700, "sha256": "dab162df80e0126149d6fd61a4d6a18b71935517fd6f4567da8ab998310ebdb2" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24625,6 +28330,13 @@ "exists": true, "size_bytes": 32231, "sha256": "eaebae0e4350e20ce66e07f762b7d2c867a06db4ad89afaac90c92bfc1d4ca61" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24667,6 +28379,13 @@ "exists": true, "size_bytes": 5324, "sha256": "f8ac257d9699d9c10673fcf5d315a0be18b2a37c88a245a16683b2cdca2bf6b3" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24709,6 +28428,13 @@ "exists": true, "size_bytes": 13118, "sha256": "b278df4853fd35b4a609df18ba48bf7d27eeba7a88d58b7ff00d759f5461ccff" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } }, { @@ -24751,6 +28477,13 @@ "exists": true, "size_bytes": 9727, "sha256": "c457f80368b1e01bab7e7d177c0bda8283d0590e7150e064c270f86c9a95e858" + }, + "t2": { + "adoption": "Refactor initial new-API staging", + "old_assertion_baseline_commit": "7438c82744e5fad2db26ee900172d7067485b5ba", + "assertion_migration_owner": "T3", + "old_assertions_obsolete": false, + "execution": "deferred to T3 and later" } } ], @@ -28753,5 +32486,49 @@ } ], "policy": "record fresh checkout WT separately from source root WT and Git blob; do not silently normalize protected files or source references; source/root bytes unchanged" + }, + "t2_introduction": { + "source_commit": "11f09eeb9460115758eada96deda1d0b08c4a442", + "production_files": 274, + "test_staging": "Refactor tracked tests copied; Dev-only files preserved. Different same-path assertions remain tracked by T1 baseline commit for T3 mapping; no assertion obsolescence decision.", + "stale_header": "Removed after imported consumers no longer reference hex8_pair_arithmetic.h.", + "dependency_helper": "Dev/Refactor Git blob hashes equal; imported working-tree bytes equal Refactor.", + "docs_target": "Preserved optional fesa_docs from Dev in imported root CMake.", + "evidence_directory": ".build/merge-verification/t2-20261018-01", + "T5_review": "Per-file t2 categories are conservative review routing, not numerical-equivalence findings; source diff preserved in production-diff.patch.", + "verification": { + "msbuild-02": { + "command": "MSBuild msvc\\fesa_cli.vcxproj /m:2 /p:Configuration=Debug /p:Platform=x64", + "exit": 0, + "seconds": 104.56687712669373, + "log": ".build\\merge-verification\\t2-20261018-01\\msbuild-02.log" + }, + "configure": { + "command": "cmake --preset windows-msvc-ninja-debug", + "exit": 0, + "seconds": 5.767545700073242, + "log": ".build\\merge-verification\\t2-20261018-01\\configure.log" + }, + "cmake-build": { + "command": "cmake --build --preset windows-msvc-ninja-debug --target fesa_cli --parallel 2", + "exit": 0, + "seconds": 79.89109897613525, + "log": ".build\\merge-verification\\t2-20261018-01\\cmake-build.log" + }, + "runtime": { + "minimal_PATH_usage_exit": { + "native": 2, + "cmake": 2 + }, + "DLL_names_and_hashes_equal": true, + "DLL_count_each": 7, + "evidence": "runtime-parity.json" + }, + "initial_wrapper_failure": { + "command": "MSBuild msvc\\fesa_cli.vcxproj /m:2 /p:Configuration=Debug /p:Platform=x64", + "exit": 123, + "cause": "Temporary Python cmd argument quoting passed full command as quoted executable; corrected temporary runner only; first log retained as msbuild.log." + } + } } } diff --git a/include/fesa/analysis/analysis_procedure.h b/include/fesa/analysis/analysis_procedure.h new file mode 100644 index 0000000..f4571cc --- /dev/null +++ b/include/fesa/analysis/analysis_procedure.h @@ -0,0 +1,23 @@ +#ifndef FESA_ANALYSIS_ANALYSIS_PROCEDURE_H_ +#define FESA_ANALYSIS_ANALYSIS_PROCEDURE_H_ + +#include "fesa/analysis/prepared_analysis_outcome.h" +#include "fesa/core/status.h" + +namespace fesa { + +class Domain; + +/// @brief Executes one prepared Domain without opening result files. +/// @note The Domain and injected adapters outlive Execute. Accepted histories +/// remain procedure-specific and are returned only after validation. +class AnalysisProcedure { + public: + virtual ~AnalysisProcedure() = default; + /// @brief Returns the original typed completed payload or first failure. + virtual Result Execute(const Domain& domain) = 0; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_ANALYSIS_PROCEDURE_H_ diff --git a/include/fesa/analysis/analysis_state.h b/include/fesa/analysis/analysis_state.h index d6d4031..0222ae2 100644 --- a/include/fesa/analysis/analysis_state.h +++ b/include/fesa/analysis/analysis_state.h @@ -6,12 +6,13 @@ #include #include "fesa/core/status.h" -#include "fesa/fem/dof_manager.h" #include "fesa/math/vector.h" #include "fesa/results/result_records.h" namespace fesa { +class DofManager; + /// @brief Owns mutable quantities required by the V0 linear-static frame. class AnalysisState { public: diff --git a/include/fesa/analysis/dynamic_execution_result.h b/include/fesa/analysis/dynamic_execution_result.h new file mode 100644 index 0000000..650a588 --- /dev/null +++ b/include/fesa/analysis/dynamic_execution_result.h @@ -0,0 +1,19 @@ +#ifndef FESA_ANALYSIS_DYNAMIC_EXECUTION_RESULT_H_ +#define FESA_ANALYSIS_DYNAMIC_EXECUTION_RESULT_H_ + +#include + +#include "fesa/analysis/dynamic_analysis_state.h" +#include "fesa/core/diagnostic.h" + +namespace fesa { + +/// @brief Stores a complete HHT history before atomic result finalization. +struct DynamicExecutionResult { + DynamicAnalysisState state; + std::vector diagnostics; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_DYNAMIC_EXECUTION_RESULT_H_ diff --git a/include/fesa/analysis/eigen_analysis_state.h b/include/fesa/analysis/eigen_analysis_state.h index 9229144..d5aecd0 100644 --- a/include/fesa/analysis/eigen_analysis_state.h +++ b/include/fesa/analysis/eigen_analysis_state.h @@ -8,7 +8,7 @@ #include #include "fesa/math/matrix.h" -#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h" +#include "fesa/solvers/eigen/eigen_solver.h" namespace fesa { diff --git a/include/fesa/analysis/hht_dynamic_procedure.h b/include/fesa/analysis/hht_dynamic_procedure.h index c10404f..082f474 100644 --- a/include/fesa/analysis/hht_dynamic_procedure.h +++ b/include/fesa/analysis/hht_dynamic_procedure.h @@ -1,10 +1,14 @@ #ifndef FESA_ANALYSIS_HHT_DYNAMIC_PROCEDURE_H_ #define FESA_ANALYSIS_HHT_DYNAMIC_PROCEDURE_H_ -#include "fesa/analysis/prepared_analysis.h" +#include "fesa/analysis/dynamic_execution_result.h" +#include "fesa/core/status.h" namespace fesa { + +class Domain; class LinearSolver; +class NewtonIterationLogWriter; class ParallelFor; class TangentSolver; diff --git a/include/fesa/analysis/linear_frequency_procedure.h b/include/fesa/analysis/linear_frequency_procedure.h index a6039b6..58cf1ad 100644 --- a/include/fesa/analysis/linear_frequency_procedure.h +++ b/include/fesa/analysis/linear_frequency_procedure.h @@ -6,13 +6,23 @@ namespace fesa { class Domain; +class EigenSolver; /// @brief Executes the supported linear MITC4 frequency procedure. class LinearFrequencyProcedureExecutor final { public: + /// @brief Preserves default FEAST construction for existing callers. + LinearFrequencyProcedureExecutor() = default; + + /// @brief Borrows a solver that must outlive this executor and its calls. + explicit LinearFrequencyProcedureExecutor(const EigenSolver& solver) : solver_(&solver) {} + /// @brief Builds, solves, and validates one frequency modal candidate. /// @return Requested physical modes plus all-c validation evidence. Result Execute(const Domain& domain) const; + + private: + const EigenSolver* solver_{nullptr}; }; } // namespace fesa diff --git a/include/fesa/analysis/linear_static_execution_result.h b/include/fesa/analysis/linear_static_execution_result.h new file mode 100644 index 0000000..c94adef --- /dev/null +++ b/include/fesa/analysis/linear_static_execution_result.h @@ -0,0 +1,19 @@ +#ifndef FESA_ANALYSIS_LINEAR_STATIC_EXECUTION_RESULT_H_ +#define FESA_ANALYSIS_LINEAR_STATIC_EXECUTION_RESULT_H_ + +#include + +#include "fesa/analysis/analysis_state.h" +#include "fesa/core/diagnostic.h" + +namespace fesa { + +/// @brief Stores a completed linear-static calculation before result writing. +struct LinearStaticExecutionResult { + AnalysisState state; + std::vector diagnostics; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_LINEAR_STATIC_EXECUTION_RESULT_H_ diff --git a/include/fesa/analysis/linear_static_procedure.h b/include/fesa/analysis/linear_static_procedure.h index fe02c04..3347d8b 100644 --- a/include/fesa/analysis/linear_static_procedure.h +++ b/include/fesa/analysis/linear_static_procedure.h @@ -1,10 +1,13 @@ #ifndef FESA_ANALYSIS_LINEAR_STATIC_PROCEDURE_H_ #define FESA_ANALYSIS_LINEAR_STATIC_PROCEDURE_H_ -#include "fesa/analysis/prepared_analysis.h" +#include "fesa/analysis/linear_static_execution_result.h" +#include "fesa/core/status.h" namespace fesa { +class Domain; + class LinearSolver; class ParallelFor; diff --git a/include/fesa/analysis/nonlinear_static_execution_result.h b/include/fesa/analysis/nonlinear_static_execution_result.h new file mode 100644 index 0000000..82ff946 --- /dev/null +++ b/include/fesa/analysis/nonlinear_static_execution_result.h @@ -0,0 +1,20 @@ +#ifndef FESA_ANALYSIS_NONLINEAR_STATIC_EXECUTION_RESULT_H_ +#define FESA_ANALYSIS_NONLINEAR_STATIC_EXECUTION_RESULT_H_ + +#include + +#include "fesa/analysis/nonlinear_analysis_state.h" +#include "fesa/core/diagnostic.h" + +namespace fesa { + +/// @brief Stores a completed nonlinear-static calculation before result +/// writing. +struct NonlinearStaticExecutionResult { + NonlinearAnalysisState state; + std::vector diagnostics; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_NONLINEAR_STATIC_EXECUTION_RESULT_H_ diff --git a/include/fesa/analysis/nonlinear_static_procedure.h b/include/fesa/analysis/nonlinear_static_procedure.h index ba12a38..2380679 100644 --- a/include/fesa/analysis/nonlinear_static_procedure.h +++ b/include/fesa/analysis/nonlinear_static_procedure.h @@ -1,10 +1,13 @@ #ifndef FESA_ANALYSIS_NONLINEAR_STATIC_PROCEDURE_H_ #define FESA_ANALYSIS_NONLINEAR_STATIC_PROCEDURE_H_ -#include "fesa/analysis/prepared_analysis.h" +#include "fesa/analysis/nonlinear_static_execution_result.h" +#include "fesa/core/status.h" namespace fesa { +class Domain; + class NewtonIterationLogWriter; class ParallelFor; class TangentSolver; diff --git a/include/fesa/analysis/prepared_analysis.h b/include/fesa/analysis/prepared_analysis.h index becbd3d..6398484 100644 --- a/include/fesa/analysis/prepared_analysis.h +++ b/include/fesa/analysis/prepared_analysis.h @@ -2,23 +2,21 @@ #define FESA_ANALYSIS_PREPARED_ANALYSIS_H_ #include -#include +#include #include -#include "fesa/analysis/analysis_state.h" -#include "fesa/analysis/dynamic_analysis_state.h" -#include "fesa/analysis/eigen_analysis_state.h" -#include "fesa/analysis/nonlinear_analysis_state.h" -#include "fesa/analysis/solid_frequency_state.h" -#include "fesa/analysis/solid_hht_dynamic_procedure.h" -#include "fesa/analysis/solid_static_procedure.h" -#include "fesa/analysis/transient_heat_procedure.h" +#include "fesa/analysis/analysis_procedure.h" +#include "fesa/analysis/prepared_analysis_outcome.h" #include "fesa/core/diagnostic.h" #include "fesa/core/status.h" #include "fesa/model/domain.h" namespace fesa { +class ParallelFor; +class LinearSolver; +class TangentSolver; +class EigenSolver; class LinearStaticProcedureExecutor; class LinearFrequencyProcedureExecutor; class EigenResultsWriter; @@ -31,6 +29,9 @@ class DynamicResultsWriter; class ThermalResultsWriter; class SolidResultsWriter; class SolidFrequencyProcedureExecutor; +class SolidStaticProcedureExecutor; +class SolidHhtDynamicProcedureExecutor; +class TransientHeatProcedureExecutor; struct DerivedOutputPaths; /// @brief Owns one parsed and semantically mapped analysis input. @@ -46,30 +47,16 @@ struct PreparedAnalysisInput { Result PrepareAnalysisInput( const std::filesystem::path& input_path); -/// @brief Stores a completed linear-static calculation before result writing. -struct LinearStaticExecutionResult { - AnalysisState state; - std::vector diagnostics; -}; +/// @brief Creates exactly one selected family with caller-owned adapters. +/// @note Domain and adapters remain alive and unmoved throughout execution. +Result> CreateAnalysisProcedure( + const Domain &domain, const ParallelFor ¶llel_for, + LinearSolver &linear_solver, TangentSolver &tangent_solver, + const EigenSolver &eigen_solver, NewtonIterationLogWriter &log); -/// @brief Stores a completed nonlinear-static calculation before result -/// writing. -struct NonlinearStaticExecutionResult { - NonlinearAnalysisState state; - std::vector diagnostics; -}; - -/// @brief Stores a complete HHT history before atomic result finalization. -struct DynamicExecutionResult { - DynamicAnalysisState state; - std::vector diagnostics; -}; - -using PreparedAnalysisOutcome = - std::variant; +/// @brief Executes a caller-selected procedure through the common interface. +Result +ExecutePreparedAnalysis(const Domain &domain, AnalysisProcedure &procedure); /// @brief Selects the approved procedure from a prepared Domain. /// @pre The executors and log outlive this call. diff --git a/include/fesa/analysis/prepared_analysis_outcome.h b/include/fesa/analysis/prepared_analysis_outcome.h new file mode 100644 index 0000000..1a9934b --- /dev/null +++ b/include/fesa/analysis/prepared_analysis_outcome.h @@ -0,0 +1,26 @@ +#ifndef FESA_ANALYSIS_PREPARED_ANALYSIS_OUTCOME_H_ +#define FESA_ANALYSIS_PREPARED_ANALYSIS_OUTCOME_H_ + +#include + +#include "fesa/analysis/dynamic_execution_result.h" +#include "fesa/analysis/eigen_analysis_state.h" +#include "fesa/analysis/linear_static_execution_result.h" +#include "fesa/analysis/nonlinear_static_execution_result.h" +#include "fesa/analysis/solid_dynamic_execution_result.h" +#include "fesa/analysis/solid_frequency_state.h" +#include "fesa/analysis/solid_static_execution_result.h" +#include "fesa/analysis/thermal_execution_result.h" + +namespace fesa { + +/// @brief Owns a completed procedure result before result-file finalization. +using PreparedAnalysisOutcome = + std::variant; + +} // namespace fesa + +#endif // FESA_ANALYSIS_PREPARED_ANALYSIS_OUTCOME_H_ diff --git a/include/fesa/analysis/solid_dynamic_execution_result.h b/include/fesa/analysis/solid_dynamic_execution_result.h new file mode 100644 index 0000000..3d78e86 --- /dev/null +++ b/include/fesa/analysis/solid_dynamic_execution_result.h @@ -0,0 +1,17 @@ +#ifndef FESA_ANALYSIS_SOLID_DYNAMIC_EXECUTION_RESULT_H_ +#define FESA_ANALYSIS_SOLID_DYNAMIC_EXECUTION_RESULT_H_ + +#include "fesa/analysis/solid_dynamic_analysis_state.h" +#include "fesa/fem/dof_manager.h" + +namespace fesa { + +/// @brief Owns the validated 3N dynamic result independently of serialization. +struct SolidDynamicExecutionResult { + DofManager dofs; + SolidDynamicAnalysisState state; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_SOLID_DYNAMIC_EXECUTION_RESULT_H_ diff --git a/include/fesa/analysis/solid_frequency_procedure.h b/include/fesa/analysis/solid_frequency_procedure.h index d779ace..b284c2c 100644 --- a/include/fesa/analysis/solid_frequency_procedure.h +++ b/include/fesa/analysis/solid_frequency_procedure.h @@ -5,14 +5,24 @@ namespace fesa { class Domain; +class EigenSolver; /// @brief Executes unloaded reference-state C3D8 generalized frequency /// analysis. class SolidFrequencyProcedureExecutor final { public: + /// @brief Preserves default FEAST construction for existing callers. + SolidFrequencyProcedureExecutor() = default; + + /// @brief Borrows a solver that must outlive this executor and its calls. + explicit SolidFrequencyProcedureExecutor(const EigenSolver& solver) : solver_(&solver) {} + /// @brief Returns lowest requested modes after validating every returned /// pair. /// @note Domain is immutable and must remain alive during execution. Result Execute(const Domain& domain) const; + + private: + const EigenSolver* solver_{nullptr}; }; } // namespace fesa #endif // FESA_ANALYSIS_SOLID_FREQUENCY_PROCEDURE_H_ diff --git a/include/fesa/analysis/solid_frequency_state.h b/include/fesa/analysis/solid_frequency_state.h index 51eb5a7..d4f1655 100644 --- a/include/fesa/analysis/solid_frequency_state.h +++ b/include/fesa/analysis/solid_frequency_state.h @@ -8,7 +8,7 @@ #include #include "fesa/math/matrix.h" -#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h" +#include "fesa/solvers/eigen/eigen_solver.h" namespace fesa { /// @brief Identifies a source-ordered physical Solid eigenmode. diff --git a/include/fesa/analysis/solid_hht_dynamic_procedure.h b/include/fesa/analysis/solid_hht_dynamic_procedure.h index b4a5e56..564839a 100644 --- a/include/fesa/analysis/solid_hht_dynamic_procedure.h +++ b/include/fesa/analysis/solid_hht_dynamic_procedure.h @@ -1,19 +1,13 @@ #ifndef FESA_ANALYSIS_SOLID_HHT_DYNAMIC_PROCEDURE_H_ #define FESA_ANALYSIS_SOLID_HHT_DYNAMIC_PROCEDURE_H_ -#include "fesa/analysis/solid_dynamic_analysis_state.h" +#include "fesa/analysis/solid_dynamic_execution_result.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 { diff --git a/include/fesa/analysis/solid_static_execution_result.h b/include/fesa/analysis/solid_static_execution_result.h new file mode 100644 index 0000000..a86c3f7 --- /dev/null +++ b/include/fesa/analysis/solid_static_execution_result.h @@ -0,0 +1,19 @@ +#ifndef FESA_ANALYSIS_SOLID_STATIC_EXECUTION_RESULT_H_ +#define FESA_ANALYSIS_SOLID_STATIC_EXECUTION_RESULT_H_ + +#include + +#include "fesa/analysis/solid_static_state.h" +#include "fesa/core/diagnostic.h" + +namespace fesa { + +/// @brief Owns successful static frames and sorted model diagnostics. +struct SolidStaticExecutionResult { + SolidStaticState state; + std::vector diagnostics; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_SOLID_STATIC_EXECUTION_RESULT_H_ diff --git a/include/fesa/analysis/solid_static_procedure.h b/include/fesa/analysis/solid_static_procedure.h index d36b418..9e67ed3 100644 --- a/include/fesa/analysis/solid_static_procedure.h +++ b/include/fesa/analysis/solid_static_procedure.h @@ -1,19 +1,13 @@ #ifndef FESA_ANALYSIS_SOLID_STATIC_PROCEDURE_H_ #define FESA_ANALYSIS_SOLID_STATIC_PROCEDURE_H_ -#include "fesa/analysis/solid_static_state.h" +#include "fesa/analysis/solid_static_execution_result.h" namespace fesa { class LinearSolver; class ParallelFor; class TangentSolver; -/// @brief Owns successful static frames and sorted model diagnostics. -struct SolidStaticExecutionResult { - SolidStaticState state; - std::vector diagnostics; -}; - /// @brief Executes linear K0 or fixed-factor TL StVK static analysis. class SolidStaticProcedureExecutor { public: diff --git a/include/fesa/analysis/solid_static_state.h b/include/fesa/analysis/solid_static_state.h index d45eae2..d487045 100644 --- a/include/fesa/analysis/solid_static_state.h +++ b/include/fesa/analysis/solid_static_state.h @@ -99,7 +99,7 @@ class SolidStaticState { const noexcept; private: - friend class SolidStaticProcedureExecutor; + friend class SolidStaticCore; bool initialized_{false}; bool nonlinear_{false}; std::vector reference_points_; diff --git a/include/fesa/analysis/thermal_analysis_state.h b/include/fesa/analysis/thermal_analysis_state.h index 1114de7..08f2948 100644 --- a/include/fesa/analysis/thermal_analysis_state.h +++ b/include/fesa/analysis/thermal_analysis_state.h @@ -6,11 +6,15 @@ #include #include "fesa/analysis/thermal_time_grid.h" -#include "fesa/assembly/thermal_assembler.h" +#include "fesa/core/status.h" #include "fesa/results/thermal_result_records.h" namespace fesa { +class AnalysisModel; +class DofManager; +class ThermalAssembly; + /// @brief Owns the immutable thermal result inventory and accepted history. /// @note No Domain/model/assembly references survive Create; state may outlive /// them. Numerical equilibrium and energy checks belong to the procedure before diff --git a/include/fesa/analysis/thermal_execution_result.h b/include/fesa/analysis/thermal_execution_result.h new file mode 100644 index 0000000..190fa8d --- /dev/null +++ b/include/fesa/analysis/thermal_execution_result.h @@ -0,0 +1,19 @@ +#ifndef FESA_ANALYSIS_THERMAL_EXECUTION_RESULT_H_ +#define FESA_ANALYSIS_THERMAL_EXECUTION_RESULT_H_ + +#include + +#include "fesa/analysis/thermal_analysis_state.h" +#include "fesa/core/diagnostic.h" + +namespace fesa { + +/// @brief Owns complete thermal history before any result output is opened. +struct ThermalExecutionResult { + ThermalAnalysisState state; + std::vector diagnostics; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_THERMAL_EXECUTION_RESULT_H_ diff --git a/include/fesa/analysis/transient_heat_procedure.h b/include/fesa/analysis/transient_heat_procedure.h index 59f7a1d..4c697d0 100644 --- a/include/fesa/analysis/transient_heat_procedure.h +++ b/include/fesa/analysis/transient_heat_procedure.h @@ -1,21 +1,14 @@ #ifndef FESA_ANALYSIS_TRANSIENT_HEAT_PROCEDURE_H_ #define FESA_ANALYSIS_TRANSIENT_HEAT_PROCEDURE_H_ -#include - -#include "fesa/analysis/thermal_analysis_state.h" +#include "fesa/analysis/thermal_execution_result.h" #include "fesa/core/status.h" namespace fesa { +class Domain; class LinearSolver; class ParallelFor; -/// @brief Owns complete thermal history before any result output is opened. -struct ThermalExecutionResult { - ThermalAnalysisState state; - std::vector diagnostics; -}; - /// @brief Executes fixed-increment backward Euler with checked thermal /// recovery. class TransientHeatProcedureExecutor final { diff --git a/include/fesa/assembly/load_assembler.h b/include/fesa/assembly/load_assembler.h index de4802f..53f4c48 100644 --- a/include/fesa/assembly/load_assembler.h +++ b/include/fesa/assembly/load_assembler.h @@ -30,7 +30,8 @@ class LoadAssembler { double step_time = 0.0); /// @brief Forms Ff-Kfc*dc in stable free/constrained order. - /// @note This operation neither factorizes nor invokes a solver. + /// @note Compatibility facade delegates to EssentialConstraintPolicy; this + /// operation neither factorizes nor invokes a solver. static Result EffectiveFreeRhs(const Vector& full_load, const SparseMatrix& kfc, const Vector& prescribed_values, diff --git a/include/fesa/assembly/thermal_assembler.h b/include/fesa/assembly/thermal_assembler.h index 2075987..443f5e6 100644 --- a/include/fesa/assembly/thermal_assembler.h +++ b/include/fesa/assembly/thermal_assembler.h @@ -2,10 +2,12 @@ #define FESA_ASSEMBLY_THERMAL_ASSEMBLER_H_ #include +#include #include #include "fesa/analysis/analysis_model.h" #include "fesa/assembly/parallel_for.h" +#include "fesa/elements/element_capabilities.h" #include "fesa/elements/thermal_shell.h" #include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" @@ -50,6 +52,8 @@ class ThermalAssembly final { const std::vector& ElementOrder() const noexcept; /// @brief Returns checked kernels in stable active element order. const std::vector& Kernels() const noexcept; + /// @brief Returns typed views bound after the final kernel buffer is placed. + const std::vector>& Elements() const noexcept; /// @brief Returns node-major twenty-entry full DOF scatters in kernel order. const std::vector>& Scatters() const noexcept; /// @brief Checks source Domain identity and the complete supplied equation @@ -79,7 +83,10 @@ class ThermalAssembly final { SparseMatrix capacity_error_; Vector input_error_; std::vector element_order_; + // Never resized after construction. Moving the owning vector transfers its + // buffer, so bound references survive ThermalAssembly moves. std::vector kernels_; + std::vector> elements_; std::vector> scatters_; }; diff --git a/include/fesa/constraints/essential_constraint_policy.h b/include/fesa/constraints/essential_constraint_policy.h index a947f50..1659c8d 100644 --- a/include/fesa/constraints/essential_constraint_policy.h +++ b/include/fesa/constraints/essential_constraint_policy.h @@ -24,6 +24,16 @@ class EssentialConstraintPolicy { Result Partition(const SparseMatrix& full_stiffness, const DofManager& dof_manager) const; + /// @brief Forms Ff-Kfc*dc in stable free/constrained order. + /// @return A finite RHS candidate or the established load diagnostic. + /// @note Preserves independent CSR products and ordered row reductions; + /// no factorization or solver operation is performed. Inputs are borrowed + /// only for this call and remain unchanged on failure. + Result EffectiveFreeRhs(const Vector& full_load, + const SparseMatrix& kfc, + const Vector& prescribed_values, + const DofManager& dofs) const; + /// @brief Gathers a full vector in stable free-equation order. Vector GatherFree(const Vector& full_values, const DofManager& dof_manager) const; diff --git a/include/fesa/constraints/prescribed_displacement.h b/include/fesa/constraints/prescribed_displacement.h index e8995ce..58a4006 100644 --- a/include/fesa/constraints/prescribed_displacement.h +++ b/include/fesa/constraints/prescribed_displacement.h @@ -5,7 +5,7 @@ #include "fesa/constraints/boundary_condition.h" #include "fesa/core/diagnostic.h" -#include "fesa/elements/element.h" +#include "fesa/elements/element_dof_layout.h" #include "fesa/model/source_target_resolver.h" namespace fesa { diff --git a/include/fesa/core/ascii.h b/include/fesa/core/ascii.h index 3fcb7ea..2072c05 100644 --- a/include/fesa/core/ascii.h +++ b/include/fesa/core/ascii.h @@ -2,6 +2,7 @@ #define FESA_CORE_ASCII_H_ #include +#include #include #include "fesa/core/status.h" @@ -12,6 +13,11 @@ namespace fesa { /// @return The lowercase ASCII byte, or the input byte when it is not A-Z. char AsciiLower(char value) noexcept; +/// @brief Returns an uppercase ASCII copy without changing other source bytes. +/// @note Embedded nulls and non-ASCII bytes are preserved without locale +/// folding. +std::string AsciiUppercase(std::string value); + /// @brief Compares two byte strings with ASCII-only case folding. /// @return True when the strings have equal length and equal ASCII-folded /// bytes. diff --git a/include/fesa/core/source_identity.h b/include/fesa/core/source_identity.h index 3890d79..876e8a8 100644 --- a/include/fesa/core/source_identity.h +++ b/include/fesa/core/source_identity.h @@ -24,6 +24,14 @@ struct SourceEntityId { std::string source_label_text; }; +/// @brief Compares all preserved source fields without canonicalizing spelling. +inline bool SourceIdentityEquals(const SourceEntityId& left, + const SourceEntityId& right) noexcept { + return left.instance_name == right.instance_name && + left.source_label == right.source_label && + left.source_label_text == right.source_label_text; +} + } // namespace fesa #endif // FESA_CORE_SOURCE_IDENTITY_H_ diff --git a/include/fesa/core/status.h b/include/fesa/core/status.h index 8476cc5..b74e7d3 100644 --- a/include/fesa/core/status.h +++ b/include/fesa/core/status.h @@ -98,11 +98,11 @@ class Result { struct FailureTag {}; /// @brief Constructs the successful value alternative. - Result(SuccessTag, T value) + Result(SuccessTag, T&& value) : value_{std::move(value)}, status_{Status::Ok()} {} /// @brief Constructs the failed status alternative. - Result(FailureTag, Status status) + Result(FailureTag, Status&& status) : value_{std::nullopt}, status_{std::move(status)} {} std::optional value_; diff --git a/include/fesa/elements/element.h b/include/fesa/elements/element.h index c0a02d7..5430172 100644 --- a/include/fesa/elements/element.h +++ b/include/fesa/elements/element.h @@ -1,13 +1,11 @@ #ifndef FESA_ELEMENTS_ELEMENT_H_ #define FESA_ELEMENTS_ELEMENT_H_ -#include -#include #include #include -#include "fesa/core/source_identity.h" #include "fesa/core/status.h" +#include "fesa/elements/element_dof_layout.h" #include "fesa/math/matrix.h" #include "fesa/math/vector.h" #include "fesa/results/result_records.h" @@ -15,23 +13,6 @@ namespace fesa { -/// @brief Identifies one component in the stable six-DOF node layout. -enum class DofComponent : std::uint8_t { - kUx, - kUy, - kUz, - kUrx, - kUry, - kUrz, -}; - -/// @brief Describes one runtime element's stable node and component order. -struct ElementDofLayout { - SourceEntityId source_id; - std::vector node_indices; - std::vector components_per_node; -}; - /// @brief Carries one element-local stiffness in its declared DOF order. struct ElementStiffnessContribution { ElementDofLayout layout; @@ -78,10 +59,6 @@ class Element { const Vector& element_displacement) const = 0; }; -/// @brief Provides non-owning runtime elements in stable owner order. -/// @note Every referenced element must outlive this view. -using ElementView = std::vector>; - } // namespace fesa #endif // FESA_ELEMENTS_ELEMENT_H_ diff --git a/include/fesa/elements/element_capabilities.h b/include/fesa/elements/element_capabilities.h new file mode 100644 index 0000000..d6c610d --- /dev/null +++ b/include/fesa/elements/element_capabilities.h @@ -0,0 +1,200 @@ +#ifndef FESA_ELEMENTS_ELEMENT_CAPABILITIES_H_ +#define FESA_ELEMENTS_ELEMENT_CAPABILITIES_H_ + +#include +#include + +#include "fesa/elements/element.h" +#include "fesa/elements/thermal_element_layout.h" +#include "fesa/math/vector3.h" + +namespace fesa { + +class Hex8Element; +class Mitc4Shell; +class NonlinearMitc4Shell; +class NonlinearMitc4Inertia; +class ThermalShell; +struct Hex8Response; +struct Hex8HighLowResponse; +struct Mitc4Stiffness; +struct Mitc4Mass; +struct NonlinearMitc4TrialDofs; +struct NonlinearMitc4InternalEquilibrium; +struct NonlinearMitc4ExternalDrillingConstraint; +struct NonlinearMitc4InertiaEvaluation; +struct ThermalShellRecovery; + +/// @brief Evaluates immutable shell trials; accepted history belongs to the procedure. +class ShellTrialElement { + public: + virtual ~ShellTrialElement() = default; + /// @brief Returns stable source-local node/component order, never equations. + virtual const ElementDofLayout& DofLayout() const noexcept = 0; + /// @brief Returns the complete force/tangent/recovery at a fixed-global trial. + /// @param coordinate_direction Optional 24-vector for D2(f_int)[v,v]. + /// @note Failure preserves the kernel, caller inputs and earlier responses. + virtual Result EvaluateTrial( + const NonlinearMitc4TrialDofs& trial, + const Vector* coordinate_direction = nullptr) const = 0; + /// @copydoc NonlinearMitc4Shell::EvaluateExternalDrillingConstraint + virtual Result + EvaluateExternalDrillingConstraint( + std::size_t node, const Vector3& phi, const Vector3& phi_dot, + const Vector3& force, const Vector3& force_rate, + const Vector3& force_acceleration) const = 0; +}; + +/// @brief Borrows the same immutable shell kernel for every trial operation. +/// @note The kernel must outlive this adapter and remain at its bound address. +class Mitc4TrialAdapter final : public ShellTrialElement { + public: + Mitc4TrialAdapter(ElementDofLayout layout, const NonlinearMitc4Shell& kernel); + const ElementDofLayout& DofLayout() const noexcept override; + Result EvaluateTrial( + const NonlinearMitc4TrialDofs& trial, + const Vector* coordinate_direction = nullptr) const override; + Result + EvaluateExternalDrillingConstraint( + std::size_t node, const Vector3& phi, const Vector3& phi_dot, + const Vector3& force, const Vector3& force_rate, + const Vector3& force_acceleration) const override; + + private: + ElementDofLayout layout_; + const NonlinearMitc4Shell& kernel_; +}; + +/// @brief Keeps complete paired solid responses and reference mass typed. +class SolidTrialElement : public Element { + public: + ~SolidTrialElement() override = default; + /// @copydoc Hex8Element::EvaluateLinear + virtual Result EvaluateLinear(const Vector& u) const = 0; + /// @copydoc Hex8Element::EvaluateNonlinear + virtual Result EvaluateNonlinear(const Vector& u) const = 0; + /// @copydoc Hex8Element::EvaluateLinearHighLow + virtual Result EvaluateLinearHighLow( + const Vector& high, const Vector& low) const = 0; + /// @copydoc Hex8Element::EvaluateNonlinearHighLow + virtual Result EvaluateNonlinearHighLow( + const Vector& high, const Vector& low) const = 0; + /// @copydoc Hex8Element::ComputeMass + virtual Result ComputeMass() const = 0; + /// @brief Returns fixed Gauss weights used by the frequency mass bound. + virtual std::array ReferenceWeights() const noexcept = 0; +}; + +/// @brief Borrows one stable Hex8 kernel shared by all local operations. +/// @note The kernel must outlive this adapter and remain at its bound address. +class Hex8TrialAdapter final : public SolidTrialElement { + public: + explicit Hex8TrialAdapter(const Hex8Element& kernel); + const ElementDofLayout& DofLayout() const noexcept override; + Result ComputeStiffness() const override; + Result Recover(const Vector& u) const override; + Result EvaluateLinear(const Vector& u) const override; + Result EvaluateNonlinear(const Vector& u) const override; + Result EvaluateLinearHighLow( + const Vector& high, const Vector& low) const override; + Result EvaluateNonlinearHighLow( + const Vector& high, const Vector& low) const override; + Result ComputeMass() const override; + std::array ReferenceWeights() const noexcept override; + + private: + const Hex8Element& kernel_; +}; + +/// @brief Returns the complete physical inertia response in fixed chart DOFs. +class ShellInertiaElement { + public: + virtual ~ShellInertiaElement() = default; + virtual const ElementDofLayout& DofLayout() const noexcept = 0; + /// @copydoc NonlinearMitc4Inertia::Evaluate + virtual Result Evaluate( + const NonlinearMitc4TrialDofs& trial, const Vector& velocity24, + const Vector& physical_acceleration24) const = 0; +}; + +/// @brief Borrows immutable inertia geometry, never shared mutable scratch. +/// @note The kernel must outlive this adapter and remain at its bound address. +class Mitc4InertiaAdapter final : public ShellInertiaElement { + public: + Mitc4InertiaAdapter(ElementDofLayout layout, + const NonlinearMitc4Inertia& kernel); + const ElementDofLayout& DofLayout() const noexcept override; + Result Evaluate( + const NonlinearMitc4TrialDofs& trial, const Vector& velocity24, + const Vector& physical_acceleration24) const override; + + private: + ElementDofLayout layout_; + const NonlinearMitc4Inertia& kernel_; +}; + +/// @brief Keeps DS4 DOFs 11-15 and all four-by-six flux traces independent. +class ThermalElement { + public: + virtual ~ThermalElement() = default; + /// @brief Returns node-major stations 11-15 and original source identity. + virtual const ThermalElementDofLayout& DofLayout() const noexcept = 0; + /// @copydoc ThermalShell::Conductivity + virtual const Matrix& Conductivity() const noexcept = 0; + /// @copydoc ThermalShell::Capacity + virtual const Matrix& Capacity() const noexcept = 0; + /// @copydoc ThermalShell::ConductivityError + virtual const Matrix& ConductivityError() const noexcept = 0; + /// @copydoc ThermalShell::CapacityError + virtual const Matrix& CapacityError() const noexcept = 0; + /// @copydoc ThermalShell::Recover + virtual Result Recover( + const std::array& temperature) const = 0; +}; + +/// @brief Borrows the same DS4 kernel for matrices, intervals and recovery. +/// @note The kernel must outlive this adapter and remain at its bound address. +class Ds4ThermalAdapter final : public ThermalElement { + public: + explicit Ds4ThermalAdapter(const ThermalShell& kernel); + const ThermalElementDofLayout& DofLayout() const noexcept override; + const Matrix& Conductivity() const noexcept override; + const Matrix& Capacity() const noexcept override; + const Matrix& ConductivityError() const noexcept override; + const Matrix& CapacityError() const noexcept override; + Result Recover( + const std::array& temperature) const override; + + private: + ThermalElementDofLayout layout_; + const ThermalShell& kernel_; +}; + +/// @brief Supplies shell frequency operators with their distinct mass contract. +class ShellReferenceElement : public Element { + public: + ~ShellReferenceElement() override = default; + /// @copydoc Mitc4Shell::Stiffness + virtual Result Stiffness() const = 0; + /// @copydoc Mitc4Shell::Mass + virtual Result Mass(double density) const = 0; +}; + +/// @brief Borrows the same linear MITC4 kernel for stiffness and recovery. +/// @note The kernel must outlive this adapter and remain at its bound address. +class Mitc4ReferenceAdapter final : public ShellReferenceElement { + public: + explicit Mitc4ReferenceAdapter(const Mitc4Shell& kernel); + const ElementDofLayout& DofLayout() const noexcept override; + Result ComputeStiffness() const override; + Result Recover(const Vector& u) const override; + Result Stiffness() const override; + Result Mass(double density) const override; + + private: + const Mitc4Shell& kernel_; +}; + +} // namespace fesa + +#endif // FESA_ELEMENTS_ELEMENT_CAPABILITIES_H_ diff --git a/include/fesa/elements/element_dof_layout.h b/include/fesa/elements/element_dof_layout.h new file mode 100644 index 0000000..15f8d9d --- /dev/null +++ b/include/fesa/elements/element_dof_layout.h @@ -0,0 +1,38 @@ +#ifndef FESA_ELEMENTS_ELEMENT_DOF_LAYOUT_H_ +#define FESA_ELEMENTS_ELEMENT_DOF_LAYOUT_H_ + +#include +#include +#include + +#include "fesa/core/source_identity.h" + +namespace fesa { + +/// @brief Identifies one component in the stable six-DOF node layout. +enum class DofComponent : std::uint8_t { + kUx, + kUy, + kUz, + kUrx, + kUry, + kUrz, +}; + +/// @brief Describes one runtime element's stable node and component order. +struct ElementDofLayout { + SourceEntityId source_id; + std::vector node_indices; + std::vector components_per_node; +}; + +class Element; + +/// @brief Provides non-owning runtime elements in stable owner order. +/// @note Every referenced element must outlive this view. +using ElementView = std::vector>; + + +} // namespace fesa + +#endif // FESA_ELEMENTS_ELEMENT_DOF_LAYOUT_H_ diff --git a/include/fesa/elements/element_factory.h b/include/fesa/elements/element_factory.h index c33a308..45867f3 100644 --- a/include/fesa/elements/element_factory.h +++ b/include/fesa/elements/element_factory.h @@ -5,6 +5,7 @@ #include "fesa/core/status.h" #include "fesa/elements/element.h" +#include "fesa/elements/element_capabilities.h" namespace fesa { @@ -21,6 +22,14 @@ class ElementFactory { Result> Create(const ElementDefinition& definition, const Domain& domain) const; + /// @brief Binds a typed view of an existing stable runtime kernel. + /// @note Returns null for a different family. The kernel must outlive the + /// returned adapter and remain at its address; no second kernel is created. + std::unique_ptr BindSolidTrial(const Element& kernel) const; + /// @copydoc BindSolidTrial + std::unique_ptr BindShellReference( + const Element& kernel) const; + private: /// @brief Creates one checked B33 runtime candidate. Result> CreateBeam( diff --git a/include/fesa/elements/euler_beam_3d.h b/include/fesa/elements/euler_beam_3d.h index cf917f6..3d61ad1 100644 --- a/include/fesa/elements/euler_beam_3d.h +++ b/include/fesa/elements/euler_beam_3d.h @@ -4,12 +4,11 @@ #include #include #include -#include #include #include "fesa/core/status.h" #include "fesa/elements/element.h" -#include "fesa/elements/element_definition.h" +#include "fesa/elements/euler_beam_3d_definition.h" #include "fesa/materials/isotropic_linear_elastic_material.h" #include "fesa/math/matrix.h" #include "fesa/math/vector.h" @@ -17,40 +16,8 @@ namespace fesa { -class Domain; struct Node; -/// @brief Defines one two-node B33 semantic element. -class EulerBeam3DDefinition final : public ElementDefinition { - public: - /// @brief Constructs a parser-validated semantic definition. - EulerBeam3DDefinition(SourceEntityId source_id, - std::array node_indices, - EntityIndex material_index, EntityIndex section_index, - SourceLocation location); - - ElementDefinitionKind Kind() const noexcept override; - const SourceEntityId& SourceId() const noexcept override; - std::string_view SourceElementType() const noexcept override; - const std::vector& NodeIndices() const noexcept override; - EntityIndex MaterialIndex() const noexcept override; - EntityIndex PropertyIndex() const noexcept override; - - SourceEntityId source_id; - std::array node_indices; - EntityIndex material_index; - EntityIndex section_index; - SourceLocation location; - - private: - friend class Domain; - - /// @brief Synchronizes the base view after parser-candidate construction. - void SynchronizeNodeIndices(); - - std::vector node_indices_view_; -}; - /// @brief Stores constant line-load components in the beam local frame. struct ConstantLocalLineLoad { double px; diff --git a/include/fesa/elements/euler_beam_3d_definition.h b/include/fesa/elements/euler_beam_3d_definition.h new file mode 100644 index 0000000..b2391ec --- /dev/null +++ b/include/fesa/elements/euler_beam_3d_definition.h @@ -0,0 +1,48 @@ +#ifndef FESA_ELEMENTS_EULER_BEAM_3D_DEFINITION_H_ +#define FESA_ELEMENTS_EULER_BEAM_3D_DEFINITION_H_ + +#include +#include +#include + +#include "fesa/elements/element_definition.h" + +namespace fesa { + +class Domain; + +/// @brief Defines one two-node B33 semantic element. +class EulerBeam3DDefinition final : public ElementDefinition { + public: + /// @brief Constructs a parser-validated semantic definition. + EulerBeam3DDefinition(SourceEntityId source_id, + std::array node_indices, + EntityIndex material_index, EntityIndex section_index, + SourceLocation location); + + ElementDefinitionKind Kind() const noexcept override; + const SourceEntityId& SourceId() const noexcept override; + std::string_view SourceElementType() const noexcept override; + const std::vector& NodeIndices() const noexcept override; + EntityIndex MaterialIndex() const noexcept override; + EntityIndex PropertyIndex() const noexcept override; + + SourceEntityId source_id; + std::array node_indices; + EntityIndex material_index; + EntityIndex section_index; + SourceLocation location; + + private: + friend class Domain; + + /// @brief Synchronizes the base view after parser-candidate construction. + void SynchronizeNodeIndices(); + + std::vector node_indices_view_; +}; + + +} // namespace fesa + +#endif // FESA_ELEMENTS_EULER_BEAM_3D_DEFINITION_H_ diff --git a/include/fesa/elements/finite_rotation.h b/include/fesa/elements/finite_rotation.h index 29513b5..19032c7 100644 --- a/include/fesa/elements/finite_rotation.h +++ b/include/fesa/elements/finite_rotation.h @@ -33,11 +33,13 @@ Matrix ExpSkew(const Vector3& phi); /// @pre phi is finite and lies inside the approved principal chart. Matrix LeftJacobianSO3(const Vector3& phi); -/// @brief Computes the inverse transpose of the SO(3) left Jacobian. -/// @param phi Fixed-global rotation coordinate in radians. -/// @return The 3-by-3 inverse-transpose left Jacobian. +/// @brief Solves J(phi)^T * moment = generalized for a spatial moment. +/// @param phi Fixed-global principal-chart rotation coordinate in radians. +/// @param generalized Work-conjugate moment in fixed-global chart coordinates. +/// @return A finite spatial moment in the same force-length units, or failure. /// @pre phi is finite and lies inside the approved principal chart. -Matrix InverseTransposeLeftJacobianSO3(const Vector3& phi); +/// @note Inputs remain unchanged; no inverse Jacobian is constructed. +Result SpatialMomentSO3(const Vector3& phi, const Vector3& generalized); /// @brief Builds a checked finite-rotation state from its reference frame. /// @param phi Fixed-global principal-chart rotation coordinate in radians. diff --git a/include/fesa/elements/mitc4_shell.h b/include/fesa/elements/mitc4_shell.h index ec16e5e..36e954d 100644 --- a/include/fesa/elements/mitc4_shell.h +++ b/include/fesa/elements/mitc4_shell.h @@ -3,12 +3,11 @@ #include #include -#include #include #include "fesa/core/status.h" #include "fesa/elements/element.h" -#include "fesa/elements/element_definition.h" +#include "fesa/elements/mitc4_shell_definition.h" #include "fesa/materials/isotropic_linear_elastic_material.h" #include "fesa/math/matrix.h" #include "fesa/math/vector.h" @@ -17,52 +16,8 @@ namespace fesa { -class Domain; struct Node; -/// @brief Preserves the source shell type independently of formulation. -enum class ShellSourceElementType { kS4, kS4r }; - -/// @brief Names the internal shell formulation selected by S4 and S4R. -inline constexpr std::string_view kMitc4InternalFormulation{"FESA-MITC4"}; - -/// @brief Defines one four-node S4/S4R semantic element. -class Mitc4ShellDefinition final : public ElementDefinition { - public: - /// @brief Constructs a parser-validated semantic definition. - Mitc4ShellDefinition(SourceEntityId source_id, - ShellSourceElementType source_type, - std::array node_indices, - EntityIndex material_index, EntityIndex section_index, - SourceLocation location); - - ElementDefinitionKind Kind() const noexcept override; - const SourceEntityId& SourceId() const noexcept override; - std::string_view SourceElementType() const noexcept override; - const std::vector& NodeIndices() const noexcept override; - EntityIndex MaterialIndex() const noexcept override; - EntityIndex PropertyIndex() const noexcept override; - - SourceEntityId source_id; - ShellSourceElementType source_type; - std::array node_indices; - EntityIndex material_index; - EntityIndex section_index; - SourceLocation location; - - private: - friend class Domain; - - /// @brief Rebinds a shell-local section index to the unified property view. - void SetPropertyIndex(EntityIndex property_index) noexcept; - - /// @brief Synchronizes the base view after parser-candidate construction. - void SynchronizeNodeIndices(); - - EntityIndex property_index_; - std::vector node_indices_view_; -}; - /// @brief Stores bilinear shape values and natural-coordinate derivatives. struct Mitc4ShapeFunctions { std::array values; @@ -213,11 +168,10 @@ class Mitc4Shell final : public Element { void BindRuntime(ElementDofLayout layout, EntityIndex element_index, SourceLocation location); - /// @brief Stores covariant, reciprocal, frame, and Jacobian data at one + /// @brief Stores covariant, frame, and Jacobian data at one /// point. struct GeometryData { std::array covariant; - std::array reciprocal; Mitc4LocalFrame frame; double jacobian; }; diff --git a/include/fesa/elements/mitc4_shell_definition.h b/include/fesa/elements/mitc4_shell_definition.h new file mode 100644 index 0000000..f7a352f --- /dev/null +++ b/include/fesa/elements/mitc4_shell_definition.h @@ -0,0 +1,60 @@ +#ifndef FESA_ELEMENTS_MITC4_SHELL_DEFINITION_H_ +#define FESA_ELEMENTS_MITC4_SHELL_DEFINITION_H_ + +#include +#include +#include + +#include "fesa/elements/element_definition.h" + +namespace fesa { + +class Domain; + +/// @brief Preserves the source shell type independently of formulation. +enum class ShellSourceElementType { kS4, kS4r }; + +/// @brief Names the internal shell formulation selected by S4 and S4R. +inline constexpr std::string_view kMitc4InternalFormulation{"FESA-MITC4"}; + +/// @brief Defines one four-node S4/S4R semantic element. +class Mitc4ShellDefinition final : public ElementDefinition { + public: + /// @brief Constructs a parser-validated semantic definition. + Mitc4ShellDefinition(SourceEntityId source_id, + ShellSourceElementType source_type, + std::array node_indices, + EntityIndex material_index, EntityIndex section_index, + SourceLocation location); + + ElementDefinitionKind Kind() const noexcept override; + const SourceEntityId& SourceId() const noexcept override; + std::string_view SourceElementType() const noexcept override; + const std::vector& NodeIndices() const noexcept override; + EntityIndex MaterialIndex() const noexcept override; + EntityIndex PropertyIndex() const noexcept override; + + SourceEntityId source_id; + ShellSourceElementType source_type; + std::array node_indices; + EntityIndex material_index; + EntityIndex section_index; + SourceLocation location; + + private: + friend class Domain; + + /// @brief Rebinds a shell-local section index to the unified property view. + void SetPropertyIndex(EntityIndex property_index) noexcept; + + /// @brief Synchronizes the base view after parser-candidate construction. + void SynchronizeNodeIndices(); + + EntityIndex property_index_; + std::vector node_indices_view_; +}; + + +} // namespace fesa + +#endif // FESA_ELEMENTS_MITC4_SHELL_DEFINITION_H_ diff --git a/include/fesa/elements/thermal_dof_component.h b/include/fesa/elements/thermal_dof_component.h new file mode 100644 index 0000000..0d25898 --- /dev/null +++ b/include/fesa/elements/thermal_dof_component.h @@ -0,0 +1,20 @@ +#ifndef FESA_ELEMENTS_THERMAL_DOF_COMPONENT_H_ +#define FESA_ELEMENTS_THERMAL_DOF_COMPONENT_H_ + + +namespace fesa { + +/// @brief Identifies the fixed bottom-to-top thermal station source DOFs. +/// @note This is separate from the six-component structural DofComponent. +enum class ThermalDofComponent : int { + kBottom = 11, + kLower = 12, + kMiddle = 13, + kUpper = 14, + kTop = 15 +}; + + +} // namespace fesa + +#endif // FESA_ELEMENTS_THERMAL_DOF_COMPONENT_H_ diff --git a/include/fesa/elements/thermal_element_layout.h b/include/fesa/elements/thermal_element_layout.h index d92b03e..4c3ce57 100644 --- a/include/fesa/elements/thermal_element_layout.h +++ b/include/fesa/elements/thermal_element_layout.h @@ -3,7 +3,8 @@ #include -#include "fesa/model/model_types.h" +#include "fesa/core/source_identity.h" +#include "fesa/elements/thermal_dof_component.h" namespace fesa { diff --git a/include/fesa/fem/dof_manager.h b/include/fesa/fem/dof_manager.h index afb3703..bfc56f1 100644 --- a/include/fesa/fem/dof_manager.h +++ b/include/fesa/fem/dof_manager.h @@ -6,25 +6,20 @@ #include #include -#include "fesa/analysis/analysis_model.h" #include "fesa/constraints/boundary_condition.h" -#include "fesa/elements/element.h" +#include "fesa/elements/element_dof_layout.h" #include "fesa/elements/thermal_element_layout.h" +#include "fesa/math/sparse_pattern.h" #include "fesa/math/vector.h" namespace fesa { +class AnalysisModel; class DofManagerTestAccess; /// @brief Distinguishes six-component, Solid translation, and thermal maps. enum class DofProfile { kStructural, kThermal, kSolidStructural }; -/// @brief Stores the stable full-space CSR pattern. -struct SparsePattern { - std::vector row_offsets; - std::vector column_indices; -}; - /// @brief Owns full/free/constrained numbering, scatter maps, and CSR pattern. class DofManager { public: diff --git a/include/fesa/math/matrix.h b/include/fesa/math/matrix.h index af51673..da79076 100644 --- a/include/fesa/math/matrix.h +++ b/include/fesa/math/matrix.h @@ -32,6 +32,26 @@ class Matrix { /// @brief Returns the column count. std::size_t Columns() const noexcept; + /// @brief Returns mutable row-major contiguous storage. + /// @note Borrows Rows()*Columns() entries until storage is moved, assigned + /// or destroyed. + double* Data() noexcept; + + /// @brief Returns immutable row-major contiguous storage. + /// @note Borrows Rows()*Columns() entries until storage is moved, assigned + /// or destroyed. + const double* Data() const noexcept; + + /// @brief Computes the robust Frobenius norm through the dense backend. + /// @note Empty storage has zero norm; nonfinite values propagate as in Vector. + /// @throws std::length_error if storage exceeds the backend integer range. + double Norm() const; + + /// @brief Scales every entry by alpha through the dense backend. + /// @note Empty storage is unchanged; nonfinite values follow BLAS semantics. + /// @throws std::length_error if storage exceeds the backend integer range. + void Scale(double alpha); + /// @brief Returns a bounds-checked mutable entry. /// @throws std::out_of_range if the index is outside the matrix. double& operator()(std::size_t row, std::size_t column); diff --git a/include/fesa/math/sparse_matrix.h b/include/fesa/math/sparse_matrix.h index 1aabf21..b3cbdd9 100644 --- a/include/fesa/math/sparse_matrix.h +++ b/include/fesa/math/sparse_matrix.h @@ -5,12 +5,11 @@ #include #include "fesa/core/status.h" +#include "fesa/math/sparse_pattern.h" #include "fesa/math/vector.h" namespace fesa { -struct SparsePattern; - /// @brief Carries one deterministic element-local COO contribution. struct CooContribution { std::size_t row; diff --git a/include/fesa/math/sparse_pattern.h b/include/fesa/math/sparse_pattern.h new file mode 100644 index 0000000..b2e9774 --- /dev/null +++ b/include/fesa/math/sparse_pattern.h @@ -0,0 +1,18 @@ +#ifndef FESA_MATH_SPARSE_PATTERN_H_ +#define FESA_MATH_SPARSE_PATTERN_H_ + +#include +#include + +namespace fesa { + +/// @brief Stores the stable full-space CSR pattern. +struct SparsePattern { + std::vector row_offsets; + std::vector column_indices; +}; + + +} // namespace fesa + +#endif // FESA_MATH_SPARSE_PATTERN_H_ diff --git a/include/fesa/model/domain.h b/include/fesa/model/domain.h index 43f8a85..54c8136 100644 --- a/include/fesa/model/domain.h +++ b/include/fesa/model/domain.h @@ -11,6 +11,8 @@ #include "fesa/constraints/boundary_condition.h" #include "fesa/constraints/prescribed_displacement.h" #include "fesa/core/status.h" +#include "fesa/elements/ds4_thermal.h" +#include "fesa/materials/isotropic_thermal_material.h" #include "fesa/loads/concentrated_nodal_load.h" #include "fesa/loads/load.h" #include "fesa/model/model_types.h" diff --git a/include/fesa/model/model_types.h b/include/fesa/model/model_types.h index 84e4bb9..88e8f7b 100644 --- a/include/fesa/model/model_types.h +++ b/include/fesa/model/model_types.h @@ -13,12 +13,11 @@ #include "fesa/core/diagnostic.h" #include "fesa/core/source_identity.h" -#include "fesa/elements/ds4_thermal.h" -#include "fesa/elements/euler_beam_3d.h" +#include "fesa/elements/euler_beam_3d_definition.h" #include "fesa/elements/hex8_definition.h" -#include "fesa/elements/mitc4_shell.h" +#include "fesa/elements/mitc4_shell_definition.h" +#include "fesa/elements/thermal_dof_component.h" #include "fesa/materials/isotropic_linear_elastic_material.h" -#include "fesa/materials/isotropic_thermal_material.h" #include "fesa/properties/general_beam_section.h" #include "fesa/properties/shell_section.h" #include "fesa/properties/solid_section.h" @@ -302,16 +301,6 @@ struct ThermalInitialConditionDefinition { SourceLocation location; }; -/// @brief Identifies the fixed bottom-to-top thermal station source DOFs. -/// @note This is separate from the six-component structural DofComponent. -enum class ThermalDofComponent : int { - kBottom = 11, - kLower = 12, - kMiddle = 13, - kUpper = 14, - kTop = 15 -}; - /// @brief Stores one prescribed thermal source DOF value. struct ThermalBoundaryConditionDefinition { EntityIndex node_index; diff --git a/include/fesa/model/source_target_resolver.h b/include/fesa/model/source_target_resolver.h index 197dbb1..697e2e3 100644 --- a/include/fesa/model/source_target_resolver.h +++ b/include/fesa/model/source_target_resolver.h @@ -38,6 +38,14 @@ class SourceTargetIndex { /// @return An owning index that preserves Domain declaration order. static SourceTargetIndex FromDomain(const Domain& domain); + /// @brief Copies node and set identities before Domain ownership begins. + /// @param nodes Semantic nodes in stable expanded declaration order. + /// @param node_sets Node memberships in stable source order. + /// @return An owning node-only index preserving raw spelling and scope. + /// @note The returned index outlives its inputs; no Domain is constructed. + static SourceTargetIndex FromNodeDefinitions( + const std::vector& nodes, const std::vector& node_sets); + /// @brief Returns owned entries without exposing mutable index state. const std::vector& Entries() const noexcept; diff --git a/include/fesa/results/prepared_results_writer.h b/include/fesa/results/prepared_results_writer.h new file mode 100644 index 0000000..6f49568 --- /dev/null +++ b/include/fesa/results/prepared_results_writer.h @@ -0,0 +1,39 @@ +#ifndef FESA_RESULTS_PREPARED_RESULTS_WRITER_H_ +#define FESA_RESULTS_PREPARED_RESULTS_WRITER_H_ + +#include +#include + +#include "fesa/analysis/prepared_analysis_outcome.h" +#include "fesa/core/diagnostic.h" +#include "fesa/core/status.h" + +namespace fesa { + +class Domain; +class ResultsWriter; +class NonlinearResultsWriter; +class EigenResultsWriter; +class DynamicResultsWriter; +class ThermalResultsWriter; +class SolidResultsWriter; + +/// @brief Routes one completed typed outcome to its authoritative writer. +/// @pre Domain, outcome, diagnostics, and adapters remain alive for this call. +/// The caller has closed its analysis log successfully before publication. +/// @param results_path Explicit output path, retaining each writer's contract. +/// @note Optional adapters are required only for their matching outcome family. +/// @return The selected writer's Status after candidate validation/finalization, +/// or the existing missing-adapter output failure without invoking a writer. +Status WritePreparedAnalysisResult( + const std::filesystem::path& results_path, const Domain& domain, + const PreparedAnalysisOutcome& outcome, + const std::vector& diagnostics, ResultsWriter& linear_writer, + NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer, + DynamicResultsWriter* dynamic_writer = nullptr, + ThermalResultsWriter* thermal_writer = nullptr, + SolidResultsWriter* solid_writer = nullptr); + +} // namespace fesa + +#endif // FESA_RESULTS_PREPARED_RESULTS_WRITER_H_ diff --git a/include/fesa/solvers/eigen/eigen_solver.h b/include/fesa/solvers/eigen/eigen_solver.h new file mode 100644 index 0000000..ecfb8e7 --- /dev/null +++ b/include/fesa/solvers/eigen/eigen_solver.h @@ -0,0 +1,99 @@ +#ifndef FESA_SOLVERS_EIGEN_EIGEN_SOLVER_H_ +#define FESA_SOLVERS_EIGEN_EIGEN_SOLVER_H_ + +#include +#include +#include +#include + +#include "fesa/core/status.h" +#include "fesa/math/matrix.h" + +namespace fesa { + +class SparseMatrix; + +/// @brief Records one deterministic private-backend invocation. +struct FeastBackendCall { + std::size_t ordinal; + std::string kind; + std::string purpose; + double lower; + double upper; + std::size_t allocated_m0; + int info; + bool count_available; + std::size_t count; + bool loop_available; + int loop; + bool epsout_available; + double epsout; + std::string outcome; + std::string decision_reason; +}; + +/// @brief Owns the complete validated FEAST interval result before public N +/// selection. +struct FeastEigenResult { + std::size_t requested_modes; + std::vector eigenvalues; + Matrix eigenvectors; + double operator_scale; + std::vector column_ids; + std::vector backend_res_available; + std::vector backend_res; + std::vector normalized_eigenvalues; + std::vector whitened_residuals; + double gram_error; + std::vector backend_calls; + std::vector base_fpm; + std::string runtime_version; + std::string integer_abi; + std::size_t thread_count; + std::uint64_t seed; + double enclosure_lower; + double enclosure_upper; + double solve_lower; + double solve_upper; + std::string upper_search_termination; +}; + +/// @brief Solves symmetric standard or definite generalized eigenproblems. +/// @note Inputs remain caller-owned and are borrowed only for each call. +class EigenSolver { + public: + /// @brief Destroys a backend-neutral eigen solver. + virtual ~EigenSolver() = default; + + /// @brief Computes all validated interval pairs for a requested lowest-mode + /// prefix. + /// @param whitened_operator Symmetric physical-lambda operator after + /// whitening. + /// @param requested_modes Number of lowest modes eventually exposed by the + /// caller. + /// @param zero_operator_scale Positive physical scale used only when A is + /// exactly zero. + /// @return Sorted all-c eigenpairs in physical lambda units or a solver + /// failure. + virtual Result Solve( + const SparseMatrix& whitened_operator, std::size_t requested_modes, + double zero_operator_scale) const = 0; + + /// @brief Solves the original sparse symmetric definite K/M pencil. + /// @param mass_lower_bound Certified positive lower spectral bound of M, + /// including floating-point assembly uncertainty; never a diagonal estimate. + /// @param classification_scale Positive max |Kii|/Mii in physical units. + /// @note Eigenvectors are mass-normalized. For this API the historical + /// whitened_residuals field contains original-pencil eta_r; no whitening + /// is performed. Intervals/eigenvalues in normalized backend evidence use + /// classification_scale, while eigenvalues are returned in physical units. + virtual Result SolveGeneralized( + const SparseMatrix& stiffness, const SparseMatrix& mass, + std::size_t requested_modes, double mass_lower_bound, + double classification_scale) const = 0; + +}; + +} // namespace fesa + +#endif // FESA_SOLVERS_EIGEN_EIGEN_SOLVER_H_ diff --git a/include/fesa/solvers/eigen/mkl_feast_eigen_solver.h b/include/fesa/solvers/eigen/mkl_feast_eigen_solver.h index 3e13447..029bc5f 100644 --- a/include/fesa/solvers/eigen/mkl_feast_eigen_solver.h +++ b/include/fesa/solvers/eigen/mkl_feast_eigen_solver.h @@ -1,65 +1,12 @@ #ifndef FESA_SOLVERS_EIGEN_MKL_FEAST_EIGEN_SOLVER_H_ #define FESA_SOLVERS_EIGEN_MKL_FEAST_EIGEN_SOLVER_H_ -#include -#include -#include -#include - -#include "fesa/core/status.h" -#include "fesa/math/matrix.h" +#include "fesa/solvers/eigen/eigen_solver.h" namespace fesa { -class SparseMatrix; - -/// @brief Records one deterministic private-backend invocation. -struct FeastBackendCall { - std::size_t ordinal; - std::string kind; - std::string purpose; - double lower; - double upper; - std::size_t allocated_m0; - int info; - bool count_available; - std::size_t count; - bool loop_available; - int loop; - bool epsout_available; - double epsout; - std::string outcome; - std::string decision_reason; -}; - -/// @brief Owns the complete validated FEAST interval result before public N -/// selection. -struct FeastEigenResult { - std::size_t requested_modes; - std::vector eigenvalues; - Matrix eigenvectors; - double operator_scale; - std::vector column_ids; - std::vector backend_res_available; - std::vector backend_res; - std::vector normalized_eigenvalues; - std::vector whitened_residuals; - double gram_error; - std::vector backend_calls; - std::vector base_fpm; - std::string runtime_version; - std::string integer_abi; - std::size_t thread_count; - std::uint64_t seed; - double enclosure_lower; - double enclosure_upper; - double solve_lower; - double solve_upper; - std::string upper_search_termination; -}; - /// @brief Adapts standard and symmetric-definite sparse pencils to FEAST. -class MklFeastEigenSolver final { +class MklFeastEigenSolver final : public EigenSolver { public: /// @brief Computes all validated interval pairs for a requested lowest-mode /// prefix. @@ -74,8 +21,8 @@ class MklFeastEigenSolver final { /// @note The implementation fixes oneMKL local thread count to one during /// every call. Result Solve(const SparseMatrix& whitened_operator, - std::size_t requested_modes, - double zero_operator_scale) const; + std::size_t requested_modes, + double zero_operator_scale) const override; /// @brief Solves the original sparse symmetric definite K/M pencil. /// @param mass_lower_bound Certified positive lower spectral bound of M, /// including floating-point assembly uncertainty; never a diagonal estimate. @@ -86,16 +33,16 @@ class MklFeastEigenSolver final { /// classification_scale, while eigenvalues are returned in physical units. Result SolveGeneralized(const SparseMatrix& stiffness, const SparseMatrix& mass, - std::size_t requested_modes, + std::size_t requested_modes, double mass_lower_bound, - double classification_scale) const; + double classification_scale) const override; private: /// @brief Shares deterministic count/search controls without changing /// pencils. Result SolveImpl(const SparseMatrix& stiffness, const SparseMatrix* mass, - std::size_t requested_modes, + std::size_t requested_modes, double physical_scale, double mass_lower_bound) const; }; diff --git a/msvc/AGENTS.md b/msvc/AGENTS.md new file mode 100644 index 0000000..a7f49eb --- /dev/null +++ b/msvc/AGENTS.md @@ -0,0 +1,17 @@ +# 직접 유지하는 MSVC 빌드 + +이 영역은 FESA 솔루션의 solver library·CLI·별도 테스트 프로젝트와 공통 옵션·DLL 준비를 +소유한다. CMake가 생성한 프로젝트로 교체하거나 빌드 전에 CMake를 숨겨서 호출하지 않는다. +요소 수식·검사 본문·기준 데이터는 이 영역의 소유가 아니다. + +- Source 추가·이동·제거는 대응하는 CMake target 목록과 같은 변경 단위로 처리한다. + 한 owner가 두 목록을 대조하며 solver와 검사 본문을 빌드 경로별로 복제하지 않는다. +- `Fesa.Common.props`와 `Fesa.Runtime.targets`의 CRT·산술·define·link·runtime staging을 + 함께 확인한다. 개인 설치 경로는 환경변수 또는 ignored `Fesa.Local.props`로 주입한다. +- AVX2 옵션은 전용 Hex8 source에만 둔다. Dispatch source와 나머지 target으로 확장하지 않는다. + 외부 GoogleTest source에 FESA 제품의 `/W4 /WX` 정책을 강제하지 않는다. +- `MSBuild FESA.sln /m:2 /p:Configuration=Debug /p:Platform=x64`는 저장소 root의 + VS x64 개발 환경에서 실행한다. 별도 테스트 executable이 같은 solver library를 연결하고 + 실행 파일 곁의 configuration별 DLL도 준비됐는지 확인한다. 빌드 성공은 검사 통과가 아니다. +- 빌드 설정을 바꾸면 CMake와 실제 source·옵션·link·DLL 및 발견한 검사 identity를 대조한다. + 기존 실행 로그와 바이너리를 덮어쓰지 말고 변경 전후 근거를 구분한다. diff --git a/msvc/Fesa.Common.props b/msvc/Fesa.Common.props new file mode 100644 index 0000000..d8e4e27 --- /dev/null +++ b/msvc/Fesa.Common.props @@ -0,0 +1,75 @@ + + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\')) + $(FesaRoot).build\native\$(Configuration)\ + $(FesaRoot.Replace('\','/').TrimEnd('/')) + $(FesaBuildRoot.Replace('\','/').TrimEnd('/')) + $(FesaBuildRoot)tests\ + $(FesaBuildRoot)src\fesa\ + $(FesaBuildRoot)lib\ + $(FesaBuildRoot)obj\$(MSBuildProjectName)\ + fesa + _debug + + + /external:I"$(FESA_MKL_ROOT)\include" /external:I"$(FESA_TBB_ROOT)\include" /external:I"$(FESA_HDF5_ROOT)\include" /external:W0 + /external:I"$(FESA_HDF5_ROOT)\include" /external:I"$(FESA_GTEST_SOURCE_DIR)\googletest\include" /external:I"$(FESA_GTEST_SOURCE_DIR)\googletest" /external:W0 + $(FesaExternalOptions) /external:I"$(FESA_MKL_ROOT)\include" + + + + stdcpp17 + false + Sync + Level3 + false + Level4 + true + MultiThreadedDebugDLL + MultiThreadedDLL + Disabled + Disabled + EnableFastChecks + MaxSpeed + AnySuitable + ProgramDatabase + ProgramDatabase + Precise + WIN32;_WINDOWS;%(PreprocessorDefinitions) + NDEBUG;WIN32;_WINDOWS;%(PreprocessorDefinitions) + $(FesaRoot)include;%(AdditionalIncludeDirectories) + $(FesaRoot)include;$(FesaRoot)src\fesa;%(AdditionalIncludeDirectories) + $(FesaExternalOptions) /MP2 %(AdditionalOptions) + TurnOffAllWarnings + + + MachineX64 + Console + true + true + true + true + $(FESA_MKL_ROOT)\lib;$(FESA_OMP_ROOT)\lib;$(FESA_TBB_ROOT)\lib;$(FESA_HDF5_ROOT)\lib;%(AdditionalLibraryDirectories) + mkl_intel_ilp64_dll.lib;mkl_intel_thread_dll.lib;mkl_core_dll.lib;libiomp5md.lib;tbb12$(TbbSuffix).lib;hdf5.lib;shlwapi.lib;%(AdditionalDependencies) + + + + MKL_ILP64;H5_BUILT_AS_DYNAMIC_LIB;__TBB_NO_IMPLICIT_LINKAGE=1;%(PreprocessorDefinitions) + + + MKL_ILP64;%(PreprocessorDefinitions) + + + H5_BUILT_AS_DYNAMIC_LIB;%(PreprocessorDefinitions) + + + + $(FESA_GTEST_SOURCE_DIR)\googletest\include;$(FESA_GTEST_SOURCE_DIR)\googletest + /utf-8 /J %(AdditionalOptions) + GTEST_HAS_PTHREAD=0;_HAS_EXCEPTIONS=1;%(PreprocessorDefinitions) + + + diff --git a/msvc/Fesa.Runtime.targets b/msvc/Fesa.Runtime.targets new file mode 100644 index 0000000..f4b18f1 --- /dev/null +++ b/msvc/Fesa.Runtime.targets @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/msvc/fesa_cli.vcxproj b/msvc/fesa_cli.vcxproj new file mode 100644 index 0000000..09cca27 --- /dev/null +++ b/msvc/fesa_cli.vcxproj @@ -0,0 +1,18 @@ + + + Debugx64Releasex64 + {6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}Win32Proj10.0 + + Applicationv145true + + + + + + + + {3AEC39D9-4641-5161-9107-265EB50CA882}true + + + + diff --git a/msvc/fesa_integration_tests.vcxproj b/msvc/fesa_integration_tests.vcxproj new file mode 100644 index 0000000..94cbc55 --- /dev/null +++ b/msvc/fesa_integration_tests.vcxproj @@ -0,0 +1,30 @@ + + + Debugx64Releasex64 + {7770C26B-DBFE-505D-8788-8B7EFD9A5A70}Win32Proj10.0 + + Applicationv145true + + + + + + + + + + + + + + + + + + {3AEC39D9-4641-5161-9107-265EB50CA882}true + {03194BA4-3F3B-5399-BCAF-FB4FA47300CE}true + {6DE2CFFA-D9B3-5E14-BEBD-5DFAB2B5290B}false + + + + diff --git a/msvc/fesa_reference_tests.vcxproj b/msvc/fesa_reference_tests.vcxproj new file mode 100644 index 0000000..d44fefb --- /dev/null +++ b/msvc/fesa_reference_tests.vcxproj @@ -0,0 +1,35 @@ + + + Debugx64Releasex64 + {330D2DFB-E57D-517D-A795-F1EA620B69F0}Win32Proj10.0 + + Applicationv145true + + + FESA_TEST_SOURCE_DIR="$(FesaRootForward)";FESA_TEST_BINARY_DIR="$(FesaBuildRootForward)";%(PreprocessorDefinitions) + + + + + + + + + + + + + + + + + + + + + {3AEC39D9-4641-5161-9107-265EB50CA882}true + {03194BA4-3F3B-5399-BCAF-FB4FA47300CE}true + + + + diff --git a/msvc/fesa_solver.vcxproj b/msvc/fesa_solver.vcxproj new file mode 100644 index 0000000..897f7f5 --- /dev/null +++ b/msvc/fesa_solver.vcxproj @@ -0,0 +1,118 @@ + + + Debugx64Releasex64 + {3AEC39D9-4641-5161-9107-265EB50CA882}Win32Proj10.0 + + StaticLibraryv145true + + + FESA_HEX8_PAIR_AVX2=1;%(PreprocessorDefinitions) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AdvancedVectorExtensions2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/msvc/fesa_unit_tests.vcxproj b/msvc/fesa_unit_tests.vcxproj new file mode 100644 index 0000000..d0826de --- /dev/null +++ b/msvc/fesa_unit_tests.vcxproj @@ -0,0 +1,108 @@ + + + Debugx64Releasex64 + {42558F77-73F2-5CA7-8E3C-BB0E1646CD45}Win32Proj10.0 + + Applicationv145true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {3AEC39D9-4641-5161-9107-265EB50CA882}true + {03194BA4-3F3B-5399-BCAF-FB4FA47300CE}true + + + + diff --git a/msvc/gtest.vcxproj b/msvc/gtest.vcxproj new file mode 100644 index 0000000..508e64c --- /dev/null +++ b/msvc/gtest.vcxproj @@ -0,0 +1,19 @@ + + + Debugx64Releasex64 + {03194BA4-3F3B-5399-BCAF-FB4FA47300CE}Win32Proj10.0 + + StaticLibraryv145true + + + + + + + + + + + + + diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index 7677679..475748b 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -41,6 +41,7 @@ add_library( core/ascii.cpp core/diagnostic.cpp core/status.cpp + elements/element_capabilities.cpp elements/element_factory.cpp elements/hex8_element.cpp elements/hex8_high_low.cpp @@ -93,12 +94,14 @@ add_library( results/analysis_state_commit.cpp results/beam_result_recovery.cpp results/global_equilibrium_recovery.cpp + results/prepared_results_writer.cpp results/recovery_candidate.cpp results/result_recovery.cpp results/shell_result_recovery.cpp solvers/eigen/mkl_feast_eigen_solver.cpp solvers/linear/mkl_pardiso_solver.cpp solvers/linear/mkl_pardiso_tangent_solver.cpp + solvers/linear/pardiso_session.cpp ) target_include_directories( diff --git a/src/fesa/analysis/analysis_procedures.h b/src/fesa/analysis/analysis_procedures.h new file mode 100644 index 0000000..96f075e --- /dev/null +++ b/src/fesa/analysis/analysis_procedures.h @@ -0,0 +1,170 @@ +#ifndef FESA_ANALYSIS_ANALYSIS_PROCEDURES_H_ +#define FESA_ANALYSIS_ANALYSIS_PROCEDURES_H_ + +#include "fesa/analysis/analysis_procedure.h" +#include "fesa/analysis/hht_dynamic_procedure.h" +#include "fesa/analysis/linear_frequency_procedure.h" +#include "fesa/analysis/linear_static_procedure.h" +#include "fesa/analysis/nonlinear_static_procedure.h" +#include "fesa/analysis/prepared_analysis_outcome.h" +#include "fesa/analysis/solid_frequency_procedure.h" +#include "fesa/analysis/solid_hht_dynamic_procedure.h" +#include "fesa/analysis/solid_static_procedure.h" +#include "fesa/analysis/transient_heat_procedure.h" + +namespace fesa { + +class EigenSolver; + +/// @brief Adapts the LinearStatic lifecycle to prepared execution. +/// @note Referenced backend and log adapters must outlive this procedure. +class LinearStaticProcedure final : public AnalysisProcedure { + public: + /// @brief Injects only the adapters required by this family. + LinearStaticProcedure(const ParallelFor& parallel_for, + LinearSolver& linear_solver) + : executor_(parallel_for, linear_solver) {} + /// @brief Retains compatibility with an already configured typed executor. + explicit LinearStaticProcedure(LinearStaticProcedureExecutor executor) + : executor_(executor) {} + Result Execute(const Domain& domain) override; + + private: + LinearStaticProcedureExecutor executor_; +}; + +/// @brief Adapts the NonlinearStatic lifecycle to prepared execution. +/// @note Referenced backend and log adapters must outlive this procedure. +class NonlinearStaticProcedure final : public AnalysisProcedure { + public: + /// @brief Injects only the adapters required by this family. + NonlinearStaticProcedure(const ParallelFor& parallel_for, + TangentSolver& tangent_solver, + NewtonIterationLogWriter& log) + : executor_(parallel_for, tangent_solver), log_(log) {} + /// @brief Retains compatibility with an already configured typed executor. + explicit NonlinearStaticProcedure(NonlinearStaticProcedureExecutor executor, + NewtonIterationLogWriter& log) + : executor_(executor), log_(log) {} + Result Execute(const Domain& domain) override; + + private: + NonlinearStaticProcedureExecutor executor_; + NewtonIterationLogWriter& log_; +}; + +/// @brief Adapts the LinearFrequency lifecycle to prepared execution. +/// @note Referenced backend and log adapters must outlive this procedure. +class LinearFrequencyProcedure final : public AnalysisProcedure { + public: + /// @brief Injects only the adapters required by this family. + LinearFrequencyProcedure(const EigenSolver& eigen_solver) + : executor_(eigen_solver) {} + /// @brief Retains compatibility with an already configured typed executor. + explicit LinearFrequencyProcedure(LinearFrequencyProcedureExecutor executor) + : executor_(executor) {} + Result Execute(const Domain& domain) override; + + private: + LinearFrequencyProcedureExecutor executor_; +}; + +/// @brief Adapts the HhtDynamic lifecycle to prepared execution. +/// @note Referenced backend and log adapters must outlive this procedure. +class HhtDynamicProcedure final : public AnalysisProcedure { + public: + /// @brief Injects only the adapters required by this family. + HhtDynamicProcedure(const ParallelFor& parallel_for, + LinearSolver& positive_solver, + TangentSolver& tangent_solver, + NewtonIterationLogWriter& log) + : executor_(parallel_for, positive_solver, tangent_solver), log_(log) {} + /// @brief Retains compatibility with an already configured typed executor. + explicit HhtDynamicProcedure(HhtDynamicProcedureExecutor executor, + NewtonIterationLogWriter& log) + : executor_(executor), log_(log) {} + Result Execute(const Domain& domain) override; + + private: + HhtDynamicProcedureExecutor executor_; + NewtonIterationLogWriter& log_; +}; + +/// @brief Adapts the SolidStatic lifecycle to prepared execution. +/// @note Referenced backend and log adapters must outlive this procedure. +class SolidStaticProcedure final : public AnalysisProcedure { + public: + /// @brief Injects only the adapters required by this family. + SolidStaticProcedure(const ParallelFor& parallel_for, + LinearSolver& linear_solver, + TangentSolver& tangent_solver, + NewtonIterationLogWriter& log) + : executor_(parallel_for, linear_solver, tangent_solver), log_(log) {} + /// @brief Retains compatibility with an already configured typed executor. + explicit SolidStaticProcedure(SolidStaticProcedureExecutor executor, + NewtonIterationLogWriter& log) + : executor_(executor), log_(log) {} + Result Execute(const Domain& domain) override; + + private: + SolidStaticProcedureExecutor executor_; + NewtonIterationLogWriter& log_; +}; + +/// @brief Adapts the SolidFrequency lifecycle to prepared execution. +/// @note Referenced backend and log adapters must outlive this procedure. +class SolidFrequencyProcedure final : public AnalysisProcedure { + public: + /// @brief Injects only the adapters required by this family. + SolidFrequencyProcedure(const EigenSolver& eigen_solver) + : executor_(eigen_solver) {} + /// @brief Retains compatibility with an already configured typed executor. + explicit SolidFrequencyProcedure(SolidFrequencyProcedureExecutor executor) + : executor_(executor) {} + Result Execute(const Domain& domain) override; + + private: + SolidFrequencyProcedureExecutor executor_; +}; + +/// @brief Adapts the SolidHhtDynamic lifecycle to prepared execution. +/// @note Referenced backend and log adapters must outlive this procedure. +class SolidHhtDynamicProcedure final : public AnalysisProcedure { + public: + /// @brief Injects only the adapters required by this family. + SolidHhtDynamicProcedure(const ParallelFor& parallel_for, + LinearSolver& positive_solver, + TangentSolver& tangent_solver, + NewtonIterationLogWriter& log) + : executor_(parallel_for, positive_solver, tangent_solver), log_(log) {} + /// @brief Retains compatibility with an already configured typed executor. + explicit SolidHhtDynamicProcedure(SolidHhtDynamicProcedureExecutor executor, + NewtonIterationLogWriter& log) + : executor_(executor), log_(log) {} + Result Execute(const Domain& domain) override; + + private: + SolidHhtDynamicProcedureExecutor executor_; + NewtonIterationLogWriter& log_; +}; + +/// @brief Adapts the TransientHeat lifecycle to prepared execution. +/// @note Referenced backend and log adapters must outlive this procedure. +class TransientHeatProcedure final : public AnalysisProcedure { + public: + /// @brief Injects only the adapters required by this family. + TransientHeatProcedure(const ParallelFor& parallel_for, + LinearSolver& linear_solver) + : executor_(parallel_for, linear_solver) {} + /// @brief Retains compatibility with an already configured typed executor. + explicit TransientHeatProcedure(TransientHeatProcedureExecutor executor) + : executor_(executor) {} + Result Execute(const Domain& domain) override; + + private: + TransientHeatProcedureExecutor executor_; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_ANALYSIS_PROCEDURES_H_ diff --git a/src/fesa/analysis/analysis_state.cpp b/src/fesa/analysis/analysis_state.cpp index eb82384..786be82 100644 --- a/src/fesa/analysis/analysis_state.cpp +++ b/src/fesa/analysis/analysis_state.cpp @@ -7,9 +7,15 @@ #include #include +#include "fesa/fem/dof_manager.h" +#include "math/finite_values.h" +#include "results/shell_result_recovery.h" + namespace fesa { namespace { +using math_internal::IsFinite; + constexpr std::size_t kShellLocationsPerElement = 4U; /// @brief Creates a structured failure without mutating existing state. @@ -21,31 +27,6 @@ Status ShellCandidateFailure(const std::string& code, {{Severity::kError, code, {}, "ANALYSIS_STATE", identity, message}}); } -template -/// @brief Tests one fixed-size candidate component array for finite values. -bool IsFinite(const std::array& values) { - return std::all_of(values.begin(), values.end(), - [](const double value) { return std::isfinite(value); }); -} - -/// @brief Tests every physical shell row component for finite values. -bool IsFinite(const ShellResultRow& row) { - if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) || - !IsFinite(row.section_resultant)) { - return false; - } - for (const auto& axis : row.local_frame) { - if (!IsFinite(axis)) { - return false; - } - } - return std::all_of(row.stress.begin(), row.stress.end(), - [](const ShellSectionStressRow& stress) { - return std::isfinite(stress.zeta) && - IsFinite(stress.components); - }); -} - } // namespace AnalysisState AnalysisState::Create(const DofManager& dofs, @@ -170,7 +151,7 @@ Status AnalysisState::CommitShellResults( "Shell stress rows require BOTTOM, MIDDLE, TOP identity."); } } - if (!IsFinite(row)) { + if (!results_internal::IsFiniteShellRow(row)) { return ShellCandidateFailure( "nonfinite-shell-state-value", std::to_string(row.element), "Shell result rows must contain only finite values."); diff --git a/src/fesa/analysis/dynamic_analysis_state.cpp b/src/fesa/analysis/dynamic_analysis_state.cpp index b494aa2..3e374c0 100644 --- a/src/fesa/analysis/dynamic_analysis_state.cpp +++ b/src/fesa/analysis/dynamic_analysis_state.cpp @@ -1,18 +1,24 @@ #include "fesa/analysis/dynamic_analysis_state.h" -#include #include #include #include #include #include +#include "fesa/core/source_identity.h" #include "fesa/elements/element_definition.h" #include "fesa/elements/finite_rotation.h" +#include "math/finite_values.h" +#include "results/shell_result_recovery.h" +#include "analysis/shell_state_inventory.h" namespace fesa { namespace { +using analysis_internal::ShellElementOrder; +using math_internal::IsFinite; + constexpr std::size_t kDofsPerNode = 6U; constexpr std::size_t kAngularDofsPerNode = 3U; constexpr std::size_t kShellLocationsPerElement = 4U; @@ -28,59 +34,6 @@ Status Failure(const FailureCategory category, const std::string& message) { message}}); } -/// @brief Compares canonical and raw source identity exactly. -bool SameId(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 Checks all components of one fixed-size physical record. -template -bool IsFinite(const std::array& values) { - return std::all_of(values.begin(), values.end(), - [](double value) { return std::isfinite(value); }); -} - -/// @brief Checks every vector component before state acceptance. -bool IsFinite(const Vector& values) { - for (std::size_t index = 0U; index < values.Size(); ++index) { - if (!std::isfinite(values[index])) { - return false; - } - } - return true; -} - -/// @brief Checks all physical recovery quantities for finite values. -bool IsFinite(const ShellResultRow& row) { - if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) || - !IsFinite(row.section_resultant)) { - return false; - } - for (const auto& axis : row.local_frame) { - if (!IsFinite(axis)) { - return false; - } - } - return std::all_of(row.stress.begin(), row.stress.end(), - [](const ShellSectionStressRow& stress) { - return std::isfinite(stress.zeta) && - IsFinite(stress.components); - }); -} - -/// @brief Collects stable Domain indices for recovery identity. -std::vector ShellElementOrder(const Domain& domain) { - std::vector order; - for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { - if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) { - order.push_back(static_cast(index)); - } - } - return order; -} - /// @brief Requires complete ordered Gauss and section-position rows. Status ValidateShellRows(const DynamicAcceptedFrameState& frame, const std::vector& shell_order) { @@ -110,7 +63,7 @@ Status ValidateShellRows(const DynamicAcceptedFrameState& frame, frame.shell_results[element * kShellLocationsPerElement + point]; if (row.element != shell_order[element] || row.location != kLocations[point] || - row.natural_coordinates != natural[point] || !IsFinite(row)) { + row.natural_coordinates != natural[point] || !results_internal::IsFiniteShellRow(row)) { return Failure(FailureCategory::kSolver, "Shell recovery rows have invalid identity or values."); } @@ -228,7 +181,7 @@ Status DynamicAnalysisState::CommitAcceptedFrame( } for (std::size_t node = 0U; node < node_count; ++node) { if (frame.nodal_internal_order[node] != node || - !SameId(frame.nodal_source_ids[node], nodal_source_ids_[node]) || + !SourceIdentityEquals(frame.nodal_source_ids[node], nodal_source_ids_[node]) || !IsFinite(frame.nodal_triad[node][0U]) || !IsFinite(frame.nodal_triad[node][1U]) || !IsFinite(frame.nodal_triad[node][2U]) || diff --git a/src/fesa/analysis/eigen_reduction.cpp b/src/fesa/analysis/eigen_reduction.cpp index 2b172e4..a7e7a0d 100644 --- a/src/fesa/analysis/eigen_reduction.cpp +++ b/src/fesa/analysis/eigen_reduction.cpp @@ -162,54 +162,49 @@ Result MakeLift(const Matrix& mass, Failure("eigen-mass-conditioning", node, "A positive-space mass pivot is not resolvable.")); } - Matrix lower(positive, positive); - Matrix inverse_lower(positive, positive); - if (!dense_blas_internal::CholeskyLowerAndInverse(normalized_mp, lower, - inverse_lower)) { + const auto factor = dense_blas_internal::DenseFactor::Cholesky(normalized_mp); + if (!factor.HasValue()) { return Result::Failure( Failure("eigen-mass-conditioning", node, "A positive-space mass pivot is not resolvable.")); } + const Matrix lower = std::move(factor.Value().Lower().Value()); Matrix transposed_lower(positive, positive); - Matrix scaled_inverse_transpose(positive, positive); - const double root_scale = std::sqrt(scale); for (std::size_t i = 0; i < positive; ++i) - for (std::size_t j = 0; j < positive; ++j) { + for (std::size_t j = 0; j < positive; ++j) transposed_lower(i, j) = lower(j, i); - scaled_inverse_transpose(i, j) = inverse_lower(j, i) / root_scale; - } - const auto inverse_mass = dense_blas_internal::MultiplyTransposeLeft( - inverse_lower, inverse_lower); + const auto condition = factor.Value().ReciprocalConditionInfinity(); const auto rebuilt_mass = lower.Multiply(transposed_lower); - double norm_mp = 0.0; - double norm_inverse = 0.0; double reconstruction = 0.0; double scaled_mass_norm = 0.0; double rebuilt_mass_norm = 0.0; for (std::size_t i = 0; i < positive; ++i) { - double row_mp = 0.0; - double row_inverse = 0.0; for (std::size_t j = 0; j < positive; ++j) { - const double inverse = inverse_mass(i, j); const double rebuilt = rebuilt_mass(i, j); - row_inverse += std::abs(inverse); - row_mp += std::abs(mp(i, j) / scale); reconstruction = std::hypot(reconstruction, rebuilt - mp(i, j) / scale); scaled_mass_norm = std::hypot(scaled_mass_norm, mp(i, j) / scale); rebuilt_mass_norm = std::hypot(rebuilt_mass_norm, rebuilt); } - norm_mp = (std::max)(norm_mp, row_mp); - norm_inverse = (std::max)(norm_inverse, row_inverse); } - result.record.rcond_inf = (1.0 / norm_mp) / norm_inverse; - if (!(result.record.rcond_inf >= kConditionLimit) || + if (!condition.HasValue() || !(condition.Value() >= kConditionLimit) || !(Ratio(reconstruction, scaled_mass_norm + rebuilt_mass_norm) <= kAlgebraLimit)) { return Result::Failure(Failure( "eigen-mass-conditioning", node, "Nodal mass fails rcond_inf >= 2^-32 or Cholesky reconstruction.")); } - result.scaled_lift = basis.Multiply(scaled_inverse_transpose); + result.record.rcond_inf = condition.Value(); + // X L^T = basis/sqrt(scale), so X^T M_scaled X = I. + Matrix scaled_basis = basis; + const double root_scale = std::sqrt(scale); + for (std::size_t c = 0; c < 6U; ++c) + for (std::size_t j = 0; j < positive; ++j) + scaled_basis(c, j) /= root_scale; + auto whitened = factor.Value().SolveLowerRight(scaled_basis, true); + if (!whitened.HasValue()) + return Result::Failure(Failure( + "eigen-mass-conditioning", node, "Nodal whitening is nonfinite.")); + result.scaled_lift = std::move(whitened.Value()); for (std::size_t c = 0; c < 6U; ++c) { for (std::size_t j = 0; j < positive; ++j) { if (!std::isfinite(result.scaled_lift(c, j))) { diff --git a/src/fesa/analysis/equilibrium_algorithm.h b/src/fesa/analysis/equilibrium_algorithm.h new file mode 100644 index 0000000..b50d492 --- /dev/null +++ b/src/fesa/analysis/equilibrium_algorithm.h @@ -0,0 +1,86 @@ +#ifndef FESA_ANALYSIS_EQUILIBRIUM_ALGORITHM_H_ +#define FESA_ANALYSIS_EQUILIBRIUM_ALGORITHM_H_ + +#include +#include +#include +#include + +#include "fesa/core/status.h" + +namespace fesa { + +/// @brief Controls evaluated iteration order without owning a family candidate. +/// @note Evaluation and its referenced trial context remain alive and unmoved +/// throughout this call. A successful evaluation value means candidate ready. +class EquilibriumAlgorithm { + public: + using Evaluation = std::function(std::uint32_t)>; + virtual ~EquilibriumAlgorithm() = default; + virtual Status SolveCurrentStep(std::uint32_t first, std::uint32_t last, + const Evaluation& evaluate) = 0; +}; + +/// @brief Preserves the original inclusive full-Newton evaluation sequence. +class NewtonStepAlgorithm final : public EquilibriumAlgorithm { + public: + Status SolveCurrentStep(std::uint32_t first, std::uint32_t last, + const Evaluation& evaluate) override { + for (std::uint32_t iteration = first; iteration <= last; ++iteration) { + auto result = evaluate(iteration); + if (!result.HasValue()) return result.GetStatus(); + if (result.Value()) return Status::Ok(); + } + // Family evaluators retain their original final-evaluation failure. + return Status::Ok(); + } +}; + +/// @brief Owns one procedure-local typed evaluator and unpublished candidate. +/// @note This value is not an algorithm interface. The injected algorithm never +/// owns or type-erases Candidate, and neither callback nor context may escape. +template +class EquilibriumStep { + public: + using Evaluation = + std::function>(std::uint32_t)>; + EquilibriumStep(Evaluation evaluate, std::uint32_t maximum) + : evaluate_(std::move(evaluate)), maximum_(maximum) {} + EquilibriumStep(const EquilibriumStep&) = delete; + EquilibriumStep& operator=(const EquilibriumStep&) = delete; + Status Solve(EquilibriumAlgorithm& algorithm) { + const EquilibriumAlgorithm::Evaluation evaluate = + [this](std::uint32_t iteration) -> Result { + auto result = evaluate_(iteration); + if (!result.HasValue()) return Result::Failure(result.GetStatus()); + if (result.Value().has_value()) { + pending_ = std::move(result.Value()); + return Result::Success(true); + } + return Result::Success(false); + }; + return algorithm.SolveCurrentStep(kFirstIteration, maximum_, evaluate); + } + bool HasCandidate() const { return pending_.has_value(); } + /// @brief Transfers the validated candidate to the procedure commit boundary. + Candidate TakeCandidate() { return std::move(pending_.value()); } + + private: + Evaluation evaluate_; + std::uint32_t maximum_; + std::optional pending_; +}; + +// Static evaluations start at one; mechanical HHT corrections start at zero. +// Evaluation at the maximum still performs the existing endpoint validation. +template +using ShellFullNewtonStep = EquilibriumStep; +template +using SolidFullNewtonStep = EquilibriumStep; +template +using ReducedShellHhtStep = EquilibriumStep; +template +using SolidHhtStep = EquilibriumStep; + +} // namespace fesa +#endif // FESA_ANALYSIS_EQUILIBRIUM_ALGORITHM_H_ diff --git a/src/fesa/analysis/hht_dynamic_algebra.cpp b/src/fesa/analysis/hht_dynamic_algebra.cpp index 45745fb..09d6803 100644 --- a/src/fesa/analysis/hht_dynamic_algebra.cpp +++ b/src/fesa/analysis/hht_dynamic_algebra.cpp @@ -16,11 +16,14 @@ #include "fesa/solvers/linear/linear_solver.h" #include "fesa/solvers/linear/tangent_solver.h" #include "math/dense_blas_internal.h" +#include "math/compensated_sum.h" namespace fesa { namespace hht_internal { namespace { +using math_internal::AddCompensated; + /// @brief Attaches a fail-closed dynamic diagnostic to the source model. Status Failure(const Domain& domain, const std::string& code, const std::string& message) { @@ -148,48 +151,23 @@ Status CheckLocalMass(const Domain& domain, const SparseMatrix& matrix, return Failure(domain, "dynamic-positive-mass", "A nonempty physical mass block has no positive scale."); } - double norm = 0.0; - for (std::size_t i = 0; i < size; ++i) { - double sum = 0.0; - for (std::size_t j = 0; j < size; ++j) { - block(i, j) /= scale; - sum += std::abs(block(i, j)); - } - norm = std::max(norm, sum); - } - Matrix lower{size, size}; - Matrix inverse_lower{size, size}; - auto factor_failure = dense_blas_internal::CholeskyFailure::kNone; + for (std::size_t i = 0; i < size; ++i) + for (std::size_t j = 0; j < size; ++j) block(i, j) /= scale; try { - if (!dense_blas_internal::CholeskyLowerAndInverse( - block, lower, inverse_lower, &factor_failure)) { - if (factor_failure == dense_blas_internal::CholeskyFailure::kInverse) - return Failure( - domain, "dynamic-positive-mass-conditioning", - "A nodal free physical block reciprocal condition is below 2^-32."); + const auto factor = dense_blas_internal::DenseFactor::Cholesky(block); + if (!factor.HasValue()) return Failure( domain, "dynamic-positive-mass", "Physical mass requires strictly positive Cholesky pivots."); - } + const auto rcond = factor.Value().ReciprocalConditionInfinity(); + if (!rcond.HasValue() || !std::isfinite(rcond.Value()) || + !(rcond.Value() >= std::ldexp(1.0, -32))) + return Failure( + domain, "dynamic-positive-mass-conditioning", + "A nodal free physical block reciprocal condition is below 2^-32."); } catch (const std::exception& error) { return Failure(domain, "dynamic-positive-mass", error.what()); } - const Matrix inverse = - dense_blas_internal::MultiplyTransposeLeft(inverse_lower, inverse_lower); - std::array sums{}; - for (std::size_t column = 0; column < size; ++column) { - for (std::size_t i = 0; i < size; ++i) { - sums[i] += std::abs(inverse(i, column)); - } - } - const double inverse_norm = *std::max_element(sums.begin(), sums.end()); - const double rcond = 1.0 / norm / inverse_norm; - if (!std::isfinite(inverse_norm) || !std::isfinite(rcond) || - !(rcond >= std::ldexp(1.0, -32))) { - return Failure( - domain, "dynamic-positive-mass-conditioning", - "A nodal free physical block reciprocal condition is below 2^-32."); - } return Status::Ok(); } @@ -468,15 +446,6 @@ Result ConstraintGradient(const DynamicAssembly& assembly, std::move(entries)); } -/// @brief Accumulates one deterministic sparse contraction without lost sums. -void AddCompensated(const double term, double* sum, double* correction) { - const double next = *sum + term; - *correction += std::abs(*sum) >= std::abs(term) - ? (*sum - next) + term - : (term - next) + *sum; - *sum = next; -} - /// @brief Stores W-scaled regularity with exact inverse-action infinity norms. struct Regularity { SparseMatrix matrix; @@ -553,23 +522,10 @@ Result CheckRegularity(const Domain& domain, sum += std::abs(matrix.Value().Values()[index]); norm = std::max(norm, sum); } - Vector inverse_sums{nodes.size()}; - // Columns are discarded after their absolute row sums are accumulated; - // a global dense inverse is never retained. - for (std::size_t column = 0; column < nodes.size(); ++column) { - Vector rhs{nodes.size()}; - rhs[column] = 1.0; - Vector solution{nodes.size()}; - const Status solved = solver.SolveCorrection(rhs, solution); - if (!solved.IsOk()) return Result::Failure(solved); - const Status checked = CheckLinearSolve(matrix.Value(), rhs, solution); - if (!checked.IsOk()) return Result::Failure(checked); - for (std::size_t row = 0; row < nodes.size(); ++row) - inverse_sums[row] += std::abs(solution[row]); - } double inverse_norm = 0.0; - for (std::size_t row = 0; row < nodes.size(); ++row) - inverse_norm = std::max(inverse_norm, inverse_sums[row]); + const Status accumulated = + AccumulateCheckedCanonicalNorm(matrix.Value(), solver, inverse_norm); + if (!accumulated.IsOk()) return Result::Failure(accumulated); const double condition = 1.0 / norm / inverse_norm; if (!std::isfinite(norm) || !std::isfinite(inverse_norm) || !std::isfinite(condition) || !(condition >= std::ldexp(1.0, -32))) { @@ -662,6 +618,26 @@ Status HiddenEvidence(const Domain& domain, const DynamicAssembly& assembly, } // namespace +Status AccumulateCheckedCanonicalNorm(const SparseMatrix& matrix, + TangentSolver& solver, double& norm) { + const auto size = matrix.Rows(); + Vector row_sums{size}; + for (std::size_t column = 0U; column < size; ++column) { + Vector rhs{size}; + rhs[column] = 1.0; + Vector solution{size}; + const Status solved = solver.SolveCorrection(rhs, solution); + if (!solved.IsOk()) return solved; + const Status checked = CheckLinearSolve(matrix, rhs, solution); + if (!checked.IsOk()) return checked; + for (std::size_t row = 0U; row < size; ++row) + row_sums[row] += std::abs(solution[row]); + } + for (std::size_t row = 0U; row < size; ++row) + norm = (std::max)(norm, row_sums[row]); + return Status::Ok(); +} + double OrderedNullMetric(const SparseMatrix& metric, const std::size_t node, const Vector3& direction) { Vector left{9U}; diff --git a/src/fesa/analysis/hht_dynamic_assembly.cpp b/src/fesa/analysis/hht_dynamic_assembly.cpp index bc60030..17012ab 100644 --- a/src/fesa/analysis/hht_dynamic_assembly.cpp +++ b/src/fesa/analysis/hht_dynamic_assembly.cpp @@ -151,9 +151,24 @@ Result> BuildRuntimeShells(const Domain& domain, *material, density); if (!inertia.HasValue()) return Result>::Failure(inertia.GetStatus()); - shells.push_back({static_cast(index), - dofs.ShellElementScatter(static_cast(index)), - std::move(internal.Value()), std::move(inertia.Value())}); + auto internal_kernel = + std::make_unique(std::move(internal.Value())); + auto inertia_kernel = + std::make_unique(std::move(inertia.Value())); + ElementDofLayout layout{ + definition->SourceId(), definition->NodeIndices(), + {DofComponent::kUx, DofComponent::kUy, DofComponent::kUz, + DofComponent::kUrx, DofComponent::kUry, DofComponent::kUrz}}; + auto trial = std::make_unique(layout, *internal_kernel); + auto kinetic = std::make_unique(layout, *inertia_kernel); + auto mapped = dofs.ElementScatter(kinetic->DofLayout()); + if (!mapped.HasValue()) + return Result>::Failure(mapped.GetStatus()); + std::array scatter{}; + std::copy(mapped.Value().begin(), mapped.Value().end(), scatter.begin()); + shells.push_back({static_cast(index), scatter, + std::move(internal_kernel), std::move(inertia_kernel), + std::move(trial), std::move(kinetic)}); } return Result>::Success(std::move(shells)); } @@ -431,8 +446,8 @@ Result EvaluateAssembly( } const auto before = retain_constraints ? ReadTrialEvaluationContext() : TrialEvaluationContext{}; - auto f = EvaluateInternalResidualAndTangent( - shells[e].internal, trial, directional_third ? &v : nullptr); + auto f = shells[e].internal->EvaluateTrial( + trial, directional_third ? &v : nullptr); if (!f.HasValue()) { failures[e] = f.GetStatus(); return; @@ -441,7 +456,7 @@ Result EvaluateAssembly( const auto after = ReadTrialEvaluationContext(); if (ReusableTrialContext(before, after)) evaluation_contexts[e] = after; } - auto m = shells[e].inertia.Evaluate(trial, v, a); + auto m = shells[e].inertia->Evaluate(trial, v, a); if (!m.HasValue()) { failures[e] = m.GetStatus(); return; @@ -499,7 +514,7 @@ Result EvaluateAssembly( external_done[node] = true; const auto first = 6U * node + 3U; const auto external = - shells[e].internal.EvaluateExternalDrillingConstraint( + shells[e].internal->EvaluateExternalDrillingConstraint( local, Triple(frame.generalized_coordinates, first), Triple(frame.velocity, first), Triple(frame.external_force, first), {}, {}); @@ -660,7 +675,7 @@ Result EvaluateAssembly( record.scatter = shells[e].scatter; record.configuration = nonlinear_mitc4_internal::ConfigurationSnapshot::Read( - shells[e].internal); + *shells[e].internal_kernel); 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]]; @@ -703,11 +718,13 @@ Status CompleteEquilibrium(const Domain& domain, const DofManager& dofs, const auto first = 6U * node; const Vector3 arm = Vector3(domain.Nodes()[node].coordinates) + Triple(frame->generalized_coordinates, first); - const Matrix transform = InverseTransposeLeftJacobianSO3( - Triple(frame->generalized_coordinates, first + 3U)); + const Vector3 phi = Triple(frame->generalized_coordinates, first + 3U); for (const Vector* values : {&frame->external_force, &frame->reaction}) { const Vector3 force = Triple(*values, first); - const Vector3 moment = Transform(transform, Triple(*values, first + 3U)); + const auto spatial = SpatialMomentSO3(phi, Triple(*values, first + 3U)); + if (!spatial.HasValue()) + return Failure("Dynamic physical force or moment balance exceeds 1e-8."); + const Vector3 moment = spatial.Value(); total_force = total_force + force; total_moment = total_moment + arm.Cross(force) + moment; force_scale += force.Norm(); diff --git a/src/fesa/analysis/hht_dynamic_internal.h b/src/fesa/analysis/hht_dynamic_internal.h index c199e93..2911f41 100644 --- a/src/fesa/analysis/hht_dynamic_internal.h +++ b/src/fesa/analysis/hht_dynamic_internal.h @@ -4,11 +4,13 @@ #include #include #include +#include #include #include #include "fesa/analysis/dynamic_analysis_state.h" #include "fesa/analysis/hht_time_integration.h" +#include "fesa/elements/element_capabilities.h" #include "fesa/elements/nonlinear_mitc4_inertia.h" #include "fesa/elements/nonlinear_mitc4_shell.h" #include "fesa/math/sparse_matrix.h" @@ -25,8 +27,10 @@ namespace hht_internal { struct RuntimeShell { EntityIndex element_index; std::array scatter; - NonlinearMitc4Shell internal; - NonlinearMitc4Inertia inertia; + std::unique_ptr internal_kernel; + std::unique_ptr inertia_kernel; + std::unique_ptr internal; + std::unique_ptr inertia; }; /// @brief Captures only controls used by the current MSVC x64 local evaluator. @@ -138,6 +142,12 @@ Result ScaledFree(const SparseMatrix& matrix, Status CheckLinearSolve(const SparseMatrix& matrix, const Vector& rhs, const Vector& solution); +/// @brief Streams checked canonical solves into the caller's infinity norm. +/// @note The caller owns factorization and the initial norm. Each column is +/// discarded after ascending absolute row-sum accumulation; failures preserve norm. +Status AccumulateCheckedCanonicalNorm(const SparseMatrix& matrix, + TangentSolver& solver, double& norm); + /// @brief Finds analytic admissible node null directions without rank cuts. std::vector NullNodes( const DofManager& dofs, diff --git a/src/fesa/analysis/hht_reduced_integrator.h b/src/fesa/analysis/hht_reduced_integrator.h new file mode 100644 index 0000000..5e461a6 --- /dev/null +++ b/src/fesa/analysis/hht_reduced_integrator.h @@ -0,0 +1,74 @@ +#ifndef FESA_ANALYSIS_HHT_REDUCED_INTEGRATOR_H_ +#define FESA_ANALYSIS_HHT_REDUCED_INTEGRATOR_H_ +#include +#include "analysis/hht_reduced_internal.h" +namespace fesa::hht_reduced_internal { +struct ReducedHistory { + Vector p{0U}; + Vector velocity{0U}; + Vector acceleration{0U}; + Vector algorithmic_acceleration{0U}; + Vector z{0U}; +}; + +/// @brief Keeps every private predictor compensation channel before a trial. +struct ReducedShellPredictor { + Vector pp{0U}; + Vector vpp{0U}; + Vector coordinate_low{0U}; + Vector increment_trace{0U}; + Vector increment_trace_compensation{0U}; + Vector increment_trace_absolute_sum{0U}; +}; + +struct ReducedShellRateRequest { + const FixedChart& chart; + const ReducedHistory& history; + const Vector& trial_algorithmic; + const Vector& vpp; + bool copy_translation_rates; + const Vector& retained_velocity; + const Vector& retained_acceleration; + double h; + const HhtParameters& parameters; +}; + +struct ReducedShellTrialRates { + Vector velocity{0U}; + Vector acceleration{0U}; +}; + +/// @brief Handles private shell predictors and exact sparse rate +/// reconstruction. +/// @note Chart/root/solver are non-owning call views; no history is committed. +class ShellHhtIntegrator { +public: + virtual ~ShellHhtIntegrator() = default; + virtual ReducedShellPredictor + Predict(const ReducedHistory& history, double h, + const HhtParameters& parameters) const = 0; + /// @note Output buffers have history.p.Size() entries and do not alias any + /// request input or each other. The caller owns their allocation/lifetime. + virtual void EvaluateRates(const ReducedShellRateRequest& request, + Vector& velocity, Vector& acceleration) const = 0; + virtual Result + ReconstructPhysicalRates(const FixedChart& chart, const EmbeddingRoot& root, + const Vector& velocity, const Vector& acceleration, + TangentSolver& solver) const = 0; +}; + +/// @brief Sequences one private predictor, pending endpoint and commit. +/// @note Callables are synchronous borrowed views. They retain all original +/// evaluation/diagnostic/commit policy; no accepted history is owned here. +template +Status RunReducedHhtStep(const ShellHhtIntegrator& integrator, + const ReducedHistory& history, double h, + const HhtParameters& parameters, + Evaluate&& evaluate, Commit&& commit) { + auto predictor = integrator.Predict(history, h, parameters); + auto pending = evaluate(integrator, std::move(predictor)); + if (!pending.HasValue()) return pending.GetStatus(); + return commit(std::move(pending.Value())); +} +} // namespace fesa::hht_reduced_internal +#endif // FESA_ANALYSIS_HHT_REDUCED_INTEGRATOR_H_ diff --git a/src/fesa/analysis/hht_reduced_operators.cpp b/src/fesa/analysis/hht_reduced_operators.cpp index a34cb5a..4f0b94b 100644 --- a/src/fesa/analysis/hht_reduced_operators.cpp +++ b/src/fesa/analysis/hht_reduced_operators.cpp @@ -10,13 +10,18 @@ #include #include +#include "fesa/core/source_identity.h" #include "fesa/solvers/linear/tangent_solver.h" #include "math/dense_blas_internal.h" #include "math/high_low_arithmetic.h" +#include "math/finite_values.h" +#include "math/compensated_sum.h" namespace fesa::hht_reduced_internal { namespace { +using RateSum = math_internal::CompensatedSum; + Status Failure(const std::string& message, const std::string& code = "dynamic-invalid-state") { return Status::Failure(FailureCategory::kSolver, @@ -25,27 +30,9 @@ Status Failure(const std::string& message, } bool Finite(const Vector& values) { - for (std::size_t i = 0U; i < values.Size(); ++i) - if (!std::isfinite(values[i])) return false; - return true; + return math_internal::IsFinite(values); } -class RateSum { - public: - void Add(const double term) { - const double next = sum_ + term; - correction_ += std::abs(sum_) >= std::abs(term) - ? (sum_ - next) + term - : (term - next) + sum_; - sum_ = next; - } - double Value() const { return sum_ + correction_; } - - private: - double sum_ = 0.0; - double correction_ = 0.0; -}; - bool ExactlyEqual(const Vector& left, const Vector& right) { if (left.Size() != right.Size()) return false; for (std::size_t i = 0U; i < left.Size(); ++i) @@ -65,13 +52,6 @@ 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, @@ -98,7 +78,7 @@ const hht_internal::DirectionalShellConstraintData* MatchingConstraints( hht_internal::ReadTrialEvaluationContext())) return nullptr; const auto configuration = - nonlinear_mitc4_internal::ConfigurationSnapshot::Read(shell.internal); + nonlinear_mitc4_internal::ConfigurationSnapshot::Read(*shell.internal_kernel); if (record.configuration.source.file != configuration.source.file || record.configuration.source.line != configuration.source.line || record.configuration.identity != configuration.identity) @@ -116,7 +96,7 @@ const hht_internal::DirectionalShellConstraintData* MatchingConstraints( 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], + !SourceIdentityEquals(record.node_source_ids[node], domain.Nodes()[global_node].source_id)) return nullptr; } @@ -534,7 +514,7 @@ Result EvaluateConstraintOperators( std::optional fresh; if (saved == nullptr) { auto internal = - EvaluateInternalResidualAndTangent(shell.internal, Trial(q), &v); + shell.internal->EvaluateTrial(Trial(q), &v); if (!internal.HasValue()) return Result::Failure(internal.GetStatus()); fresh = std::move(internal.Value()); @@ -572,7 +552,7 @@ Result EvaluateConstraintOperators( if (external_done[global_node]) continue; external_done[global_node] = true; const auto first = 6U * global_node + 3U; - const auto external = shell.internal.EvaluateExternalDrillingConstraint( + const auto external = shell.internal->EvaluateExternalDrillingConstraint( local_node, Vector3{q[6U * local_node + 3U], q[6U * local_node + 4U], q[6U * local_node + 5U]}, @@ -587,7 +567,7 @@ Result EvaluateConstraintOperators( force_acceleration[first + 2U]}); if (!external.HasValue()) return Result::Failure(external.GetStatus()); - const auto rate_term = shell.internal.EvaluateExternalDrillingConstraint( + const auto rate_term = shell.internal->EvaluateExternalDrillingConstraint( local_node, Vector3{q[6U * local_node + 3U], q[6U * local_node + 4U], q[6U * local_node + 5U]}, @@ -596,7 +576,7 @@ Result EvaluateConstraintOperators( if (!rate_term.HasValue()) return Result::Failure(rate_term.GetStatus()); const auto acceleration_term = - shell.internal.EvaluateExternalDrillingConstraint( + shell.internal->EvaluateExternalDrillingConstraint( local_node, Vector3{q[6U * local_node + 3U], q[6U * local_node + 4U], q[6U * local_node + 5U]}, @@ -1321,21 +1301,10 @@ Result CheckGraphRegularity( if (!sparse.HasValue()) return Result::Failure(sparse.GetStatus()); Status status = solver.FactorizeFresh(sparse.Value()); if (!status.IsOk()) return Result::Failure(status); - Vector inverse_sums{size}; - for (std::size_t column = 0U; column < size; ++column) { - Vector rhs{size}; - rhs[column] = 1.0; - Vector solution{size}; - status = solver.SolveCorrection(rhs, solution); - if (!status.IsOk()) return Result::Failure(status); - status = hht_internal::CheckLinearSolve(sparse.Value(), rhs, solution); - if (!status.IsOk()) return Result::Failure(status); - for (std::size_t row = 0U; row < size; ++row) - inverse_sums[row] += std::abs(solution[row]); - } double inverse_norm = 0.0; - for (std::size_t row = 0U; row < size; ++row) - inverse_norm = std::max(inverse_norm, inverse_sums[row]); + status = hht_internal::AccumulateCheckedCanonicalNorm( + sparse.Value(), solver, inverse_norm); + if (!status.IsOk()) return Result::Failure(status); const double condition = 1.0 / norm / inverse_norm; if (!std::isfinite(norm) || !std::isfinite(inverse_norm) || !std::isfinite(condition) || !(condition >= std::ldexp(1.0, -32))) { @@ -1409,20 +1378,9 @@ Result CheckSelectorTransversality( if (!sparse.HasValue()) return Result::Failure(sparse.GetStatus()); auto status = solver.FactorizeFresh(sparse.Value()); if (!status.IsOk()) return Result::Failure(status); - Vector inverse_rows{rows.size()}; - for (std::size_t column = 0U; column < rows.size(); ++column) { - Vector rhs{rows.size()}; - rhs[column] = 1.0; - Vector solution{rows.size()}; - status = solver.SolveCorrection(rhs, solution); - if (!status.IsOk()) return Result::Failure(status); - status = hht_internal::CheckLinearSolve(sparse.Value(), rhs, solution); - if (!status.IsOk()) return Result::Failure(status); - for (std::size_t row = 0U; row < rows.size(); ++row) - inverse_rows[row] += std::abs(solution[row]); - } - for (std::size_t row = 0U; row < rows.size(); ++row) - inverse_norm = (std::max)(inverse_norm, inverse_rows[row]); + status = hht_internal::AccumulateCheckedCanonicalNorm( + sparse.Value(), solver, inverse_norm); + if (!status.IsOk()) return Result::Failure(status); } const double condition = 1.0 / norm / inverse_norm; if (!std::isfinite(condition) || condition < std::ldexp(1.0, -32)) diff --git a/src/fesa/analysis/hht_reduced_procedure.cpp b/src/fesa/analysis/hht_reduced_procedure.cpp index e7d9e40..efcd372 100644 --- a/src/fesa/analysis/hht_reduced_procedure.cpp +++ b/src/fesa/analysis/hht_reduced_procedure.cpp @@ -1,4 +1,7 @@ #include "analysis/hht_reduced_procedure.h" +#include "analysis/hht_reduced_integrator.h" + +#include "equilibrium_algorithm.h" #include #include @@ -296,12 +299,83 @@ Status CheckReducedUpdates(const Domain& domain, return Status::Ok(); } -struct ReducedHistory { - Vector p{0U}; - Vector velocity{0U}; - Vector acceleration{0U}; - Vector algorithmic_acceleration{0U}; - Vector z{0U}; +// Private nonvirtual primitive: keep velocity assignment before acceleration +// evaluation, including per-index position retries. No accepted state changes. +void EvaluateReducedShellRate(double predictor_velocity, + double algorithmic_acceleration, + double old_acceleration, double h, + const HhtParameters& parameters, + double* velocity, double* acceleration) { + *velocity = predictor_velocity + parameters.gamma * h * algorithmic_acceleration; + *acceleration = (algorithmic_acceleration + parameters.alpha * old_acceleration) / + (1.0 + parameters.alpha); +} + +class ReducedCoordinateShellHhtIntegrator final : public ShellHhtIntegrator { +public: + ReducedShellPredictor + Predict(const ReducedHistory& history, double h, + const HhtParameters& parameters) const override { + Vector pp{history.p.Size()}, vpp{history.p.Size()}; + for (std::size_t i = 0U; i < history.p.Size(); ++i) { + pp[i] = + history.p[i] + h * history.velocity[i] + + h * h * (0.5 - parameters.beta) * history.algorithmic_acceleration[i]; + vpp[i] = history.velocity[i] + h * (1.0 - parameters.gamma) * + history.algorithmic_acceleration[i]; + } + Vector coordinate_low{history.p.Size()}; + Vector increment_trace{history.p.Size()}; + Vector increment_trace_compensation{history.p.Size()}; + Vector increment_trace_absolute_sum{history.p.Size()}; + for (std::size_t i = 0U; i < history.p.Size(); ++i) { + const double velocity_term = h * history.velocity[i]; + const double old_acceleration_term = + h * h * (0.5 - parameters.beta) * history.algorithmic_acceleration[i]; + const double first_sum = history.p[i] + velocity_term; + coordinate_low[i] = + AdditionLowPart(history.p[i], velocity_term, first_sum) + + AdditionLowPart(first_sum, old_acceleration_term, pp[i]); + AddIncrementTraceTerm(velocity_term, &increment_trace[i], + &increment_trace_compensation[i]); + AddIncrementTraceTerm(old_acceleration_term, &increment_trace[i], + &increment_trace_compensation[i]); + increment_trace_absolute_sum[i] = + std::abs(velocity_term) + std::abs(old_acceleration_term); + } + return {std::move(pp), + std::move(vpp), + std::move(coordinate_low), + std::move(increment_trace), + std::move(increment_trace_compensation), + std::move(increment_trace_absolute_sum)}; + } + void EvaluateRates(const ReducedShellRateRequest& request, + Vector& trial_v, Vector& trial_a) const override { + const auto& history = request.history; + const auto& trial_algorithmic = request.trial_algorithmic; + const auto& vpp = request.vpp; + const auto& parameters = request.parameters; + const double h = request.h; + for (std::size_t i = 0U; i < history.p.Size(); ++i) { + if (request.copy_translation_rates && + PhysicalCoordinateChannel(request.chart, i) == 0U) { + trial_v[i] = request.retained_velocity[i]; + trial_a[i] = request.retained_acceleration[i]; + } else { + EvaluateReducedShellRate( + vpp[i], trial_algorithmic[i], history.acceleration[i], h, parameters, + &trial_v[i], &trial_a[i]); + } + } + } + Result + ReconstructPhysicalRates(const FixedChart& chart, const EmbeddingRoot& root, + const Vector& velocity, const Vector& acceleration, + TangentSolver& solver) const override { + return hht_reduced_internal::ReconstructPhysicalRates(chart, root, velocity, + acceleration, solver); + } }; struct Trial { @@ -320,6 +394,17 @@ Result ExecuteReducedHht( const Domain& domain, NewtonIterationLogWriter& log, const ParallelFor& parallel_for, LinearSolver& positive_solver, TangentSolver& tangent_solver) { + const ReducedCoordinateShellHhtIntegrator integrator; + NewtonStepAlgorithm algorithm; + return ExecuteReducedHht(domain, log, parallel_for, positive_solver, + tangent_solver, integrator, algorithm); +} + +Result ExecuteReducedHht( + const Domain& domain, NewtonIterationLogWriter& log, + const ParallelFor& parallel_for, LinearSolver& positive_solver, + TangentSolver& tangent_solver, const ShellHhtIntegrator& integrator, + EquilibriumAlgorithm& algorithm) { if (domain.Dynamic() == nullptr) return Result::Failure(Status::Failure( FailureCategory::kModel, {{Severity::kError, "invalid-dynamic-step", {}, @@ -433,7 +518,7 @@ Result ExecuteReducedHht( tangent_solver); if (!root0.HasValue()) return Result::Failure( WithProcedureContext(domain, root0.GetStatus(), "inner-position", 0U, 0.0)); - auto rates0 = ReconstructPhysicalRates(chart.Value(), root0.Value(), history.velocity, + auto rates0 = integrator.ReconstructPhysicalRates(chart.Value(), root0.Value(), history.velocity, history.acceleration, tangent_solver); if (!rates0.HasValue()) return Result::Failure( WithProcedureContext(domain, rates0.GetStatus(), "inner-rates", 0U, 0.0)); @@ -604,343 +689,434 @@ Result ExecuteReducedHht( status = log.Write(record); if (status.IsOk()) status = log.Flush(); if (!status.IsOk()) return Result::Failure(status); - for (std::uint32_t increment = 1U; increment <= domain.Dynamic()->increment_count; ++increment) { + struct ReducedStepCandidate { + DynamicAcceptedFrameState frame; + ReducedHistory history; + }; + for (std::uint32_t increment = 1U; + increment <= domain.Dynamic()->increment_count; ++increment) { const auto& old = state.CommittedState(); - Vector pp{history.p.Size()}, vpp{history.p.Size()}; - for (std::size_t i = 0U; i < history.p.Size(); ++i) { - pp[i] = history.p[i] + h * history.velocity[i] + h * h * (0.5 - parameters.beta) * history.algorithmic_acceleration[i]; - vpp[i] = history.velocity[i] + h * (1.0 - parameters.gamma) * history.algorithmic_acceleration[i]; - } - DynamicAcceptedFrameState trial_frame = old; - trial_frame.is_initial = false; trial_frame.frame_index = increment; - trial_frame.increment_index = increment; trial_frame.time = static_cast(increment) * h; - trial_frame.time_integration_formulation = DynamicTimeIntegrationFormulation::kReducedCoordinateHht; - Vector trial_algorithmic{history.p.Size()}; - Vector trial_p = pp; - Vector coordinate_low{history.p.Size()}; - Vector chosen_start_p = pp; - Vector increment_trace{history.p.Size()}; - Vector increment_trace_compensation{history.p.Size()}; - Vector increment_trace_absolute_sum{history.p.Size()}; - for (std::size_t i = 0U; i < history.p.Size(); ++i) { - const double velocity_term = h * history.velocity[i]; - const double old_acceleration_term = - h * h * (0.5 - parameters.beta) * history.algorithmic_acceleration[i]; - const double first_sum = history.p[i] + velocity_term; - coordinate_low[i] = AdditionLowPart(history.p[i], velocity_term, first_sum) + - AdditionLowPart(first_sum, old_acceleration_term, pp[i]); - AddIncrementTraceTerm(velocity_term, &increment_trace[i], - &increment_trace_compensation[i]); - AddIncrementTraceTerm(old_acceleration_term, &increment_trace[i], - &increment_trace_compensation[i]); - increment_trace_absolute_sum[i] = - std::abs(velocity_term) + std::abs(old_acceleration_term); - } - Vector seed = history.z; - record = {}; record.increment_index = increment; record.time = trial_frame.time; record.correction_pass = true; - bool translation_private_algorithmic_pass = true; - bool copy_translation_rates = false; - Vector retained_velocity{history.p.Size()}; - Vector retained_acceleration{history.p.Size()}; - bool accepted = false; - for (std::uint32_t iteration = 0U; - iteration <= kMaximumOuterCorrections; ++iteration) { - record.iteration_index = iteration; record.stage = "evaluation"; record.outcome = "candidate"; - const auto fail = [&](const Status& failure, - const char* stage = "outer") -> Result { - const Status contextual = WithProcedureContext( - domain, failure, stage, increment, trial_frame.time); - record.stage = stage; record.outcome = "failed"; - record.failure_code = contextual.Diagnostics().empty() - ? "dynamic-solver-failure" - : contextual.Diagnostics().front().code; - const Status written = log.Write(record); - return Result::Failure( - written.IsOk() ? contextual : written); - }; - Vector trial_v{history.p.Size()}, trial_a{history.p.Size()}; - for (std::size_t i = 0U; i < trial_p.Size(); ++i) { - if (copy_translation_rates && PhysicalCoordinateChannel(chart.Value(), i) == 0U) { - trial_v[i] = retained_velocity[i]; - trial_a[i] = retained_acceleration[i]; - } else { - trial_v[i] = vpp[i] + parameters.gamma * h * trial_algorithmic[i]; - trial_a[i] = (trial_algorithmic[i] + parameters.alpha * history.acceleration[i]) / (1.0 + parameters.alpha); - } - } - const auto evaluate_trial = [&](const Vector& physical_p, - const Vector& physical_v, - const Vector& physical_a, - const Vector& root_seed) -> Result { - auto root = SolveEmbeddingRoot(chart.Value(), physical_p, root_seed, - [&](const Vector& q) { return position_operators(trial_frame, q); }, - tangent_solver); - if (!root.HasValue()) return Result::Failure( - WithProcedureContext(domain, root.GetStatus(), "inner-position", - increment, trial_frame.time)); - auto rates = ReconstructPhysicalRates(chart.Value(), root.Value(), physical_v, - physical_a, tangent_solver); - if (!rates.HasValue()) return Result::Failure( - WithProcedureContext(domain, rates.GetStatus(), "inner-rates", - increment, trial_frame.time)); - auto candidate = trial_frame; - candidate.generalized_coordinates = root.Value().q; - candidate.velocity = rates.Value().velocity; - candidate.acceleration = rates.Value().acceleration; - candidate.algorithmic_acceleration = Vector(dofs.FullDofCount()); - for (std::size_t i = 0U; i < candidate.algorithmic_acceleration.Size(); ++i) - candidate.algorithmic_acceleration[i] = - (1.0 + parameters.alpha) * candidate.acceleration[i] - - parameters.alpha * old.acceleration[i]; - candidate.iteration_count = iteration; - auto assembly = EvaluateAssembly(domain, dofs, shells.Value(), candidate, - loads.Value()[1U], h, parameters, - parallel_for, true); - if (!assembly.HasValue()) return Result::Failure( - WithProcedureContext(domain, assembly.GetStatus(), "outer", - increment, trial_frame.time)); - assembly.Value().frame.time_integration_formulation = - DynamicTimeIntegrationFormulation::kReducedCoordinateHht; - return Result::Success( - {std::move(assembly.Value()), std::move(root.Value()), physical_p, - physical_v, physical_a, trial_algorithmic, - std::move(rates.Value().z_velocity), std::move(rates.Value().z_acceleration)}); - }; - position_geometry_failure = false; - auto trial = evaluate_trial(trial_p, trial_v, trial_a, seed); - if (!trial.HasValue() && iteration == 0U && position_geometry_failure) { - for (std::size_t i = 0U; i < trial_p.Size(); ++i) { - trial_algorithmic[i] = - (history.p[i] - pp[i]) / (parameters.beta * h * h); - trial_p[i] = history.p[i]; - chosen_start_p[i] = history.p[i]; - coordinate_low[i] = 0.0; - increment_trace[i] = 0.0; - increment_trace_compensation[i] = 0.0; - increment_trace_absolute_sum[i] = 0.0; - trial_v[i] = vpp[i] + parameters.gamma * h * trial_algorithmic[i]; - trial_a[i] = (trial_algorithmic[i] + - parameters.alpha * history.acceleration[i]) / - (1.0 + parameters.alpha); - } - trial = evaluate_trial(trial_p, trial_v, trial_a, seed); - } - if (!trial.HasValue()) return fail(trial.GetStatus()); - if (iteration == 0U && !ExactlyEqual(trial_p, history.p)) { - Vector old_algorithmic{trial_p.Size()}; - Vector old_velocity{trial_p.Size()}; - Vector old_acceleration{trial_p.Size()}; - for (std::size_t i = 0U; i < trial_p.Size(); ++i) { - old_algorithmic[i] = (history.p[i] - pp[i]) / (parameters.beta * h * h); - old_velocity[i] = vpp[i] + parameters.gamma * h * old_algorithmic[i]; - old_acceleration[i] = (old_algorithmic[i] + parameters.alpha * history.acceleration[i]) / - (1.0 + parameters.alpha); - } - auto old_trial = evaluate_trial(history.p, old_velocity, old_acceleration, seed); - if (!old_trial.HasValue()) return fail(old_trial.GetStatus()); - const auto reduced_residual = [&](const Trial& value) { - double norm = 0.0; - for (const auto full : dofs.FreeDofs()) { - const double scale = full % 6U < 3U ? length : 1.0; - norm = (std::max)(norm, - scale * std::abs(value.assembly.frame.residual[full])); - } - if (!std::isfinite(norm)) return std::numeric_limits::infinity(); - return norm; - }; - const double old_residual = reduced_residual(old_trial.Value()); - const double predictor_residual = reduced_residual(trial.Value()); - if (!std::isfinite(old_residual) || !std::isfinite(predictor_residual)) - return fail(Failure(domain, "dynamic-invalid-state", - "The scaled initial reduced residual is nonfinite.")); - if (old_residual < predictor_residual) { - trial = std::move(old_trial); - trial_p = history.p; - chosen_start_p = history.p; - coordinate_low = Vector(history.p.Size()); - increment_trace = Vector(history.p.Size()); - increment_trace_compensation = Vector(history.p.Size()); - increment_trace_absolute_sum = Vector(history.p.Size()); - trial_v = old_velocity; - trial_a = old_acceleration; - trial_algorithmic = old_algorithmic; - } - } - auto candidate = trial.Value().assembly.frame; - auto operators = EvaluateConstraintOperators(domain, dofs, shells.Value(), - trial.Value().assembly, chart.Value(), zero, zero); - if (!operators.HasValue()) return fail(operators.GetStatus()); - ResidualRecord(dofs, candidate, &record); - if (record.residual_pass && record.correction_pass) { - status = CompleteConstraintEvidence(domain, dofs, trial.Value().assembly, length, null_nodes, - positive_solver, tangent_solver, &candidate); - if (!status.IsOk()) return fail(status, "accepted"); - status = CompleteEquilibrium(domain, dofs, trial.Value().assembly, &candidate); - if (!status.IsOk()) return fail(status, "accepted"); - graph_regularity = CheckGraphRegularity(trial.Value().assembly, - chart.Value(), operators.Value(), - tangent_solver); - if (!graph_regularity.HasValue()) - return fail(graph_regularity.GetStatus(), "accepted"); - status = CheckReducedUpdates(domain, old, candidate, pp, vpp, trial_p, trial_v, - trial_a, trial.Value().root.z, - trial.Value().algebraic_velocity, - trial.Value().algebraic_acceleration, - trial_algorithmic, history.p, history.velocity, - history.acceleration, - history.algorithmic_acceleration, - increment_trace, - increment_trace_compensation, - increment_trace_absolute_sum, - chart.Value(), chosen_start_p, coordinate_low, - parameters, h); - if (!status.IsOk()) return fail(status, "accepted"); - candidate.external_work = 0.0; - for (std::size_t i = 0U; i < dofs.FullDofCount(); ++i) - candidate.external_work += candidate.external_force[i] * candidate.generalized_coordinates[i]; - candidate.energy_defect = candidate.kinetic_energy + candidate.physical_strain_energy + - candidate.drilling_energy - initial_energy - candidate.external_work; - status = state.CommitAcceptedFrame(candidate); - if (!status.IsOk()) return fail(status, "accepted"); - history = {trial_p, trial_v, trial_a, trial_algorithmic, trial.Value().root.z}; - record.stage = "accepted"; record.outcome = "accepted"; - status = log.Write(record); if (status.IsOk()) status = log.Flush(); - if (!status.IsOk()) return Result::Failure(status); - accepted = true; break; - } - if (!CanApplyOuterCorrection(iteration)) { - double full_hidden = 0.0; - double analytic_hidden = 0.0; - double inertia_hidden = 0.0; - double consistency = 0.0; - for (std::size_t row = 0U; row < chart.Value().null_nodes.size(); ++row) { - const auto node = chart.Value().null_nodes[row]; - const auto& null = trial.Value().assembly.rotations[node].drill_null; - double full = 0.0; - double inertia = 0.0; - for (std::size_t component = 0U; component < 3U; ++component) { - const auto full_dof = 6U * node + 3U + component; - full += null[component] * candidate.residual[full_dof]; - inertia += null[component] * candidate.inertia_force[full_dof]; - } - const double analytic = operators.Value().rows[row].value; - full_hidden = (std::max)(full_hidden, std::abs(full)); - analytic_hidden = (std::max)(analytic_hidden, std::abs(analytic)); - inertia_hidden = (std::max)(inertia_hidden, std::abs(inertia)); - consistency = (std::max)(consistency, std::abs(full - analytic - inertia)); - } - std::ostringstream message; - const auto maximum = [](const Vector& values) { - double result = 0.0; - for (std::size_t i = 0U; i < values.Size(); ++i) - result = (std::max)(result, std::abs(values[i])); - return result; - }; - Vector position_correction{trial_algorithmic.Size()}; - Vector velocity_correction{trial_algorithmic.Size()}; - for (std::size_t i = 0U; i < trial_algorithmic.Size(); ++i) { - position_correction[i] = parameters.beta * h * h * trial_algorithmic[i]; - velocity_correction[i] = parameters.gamma * h * trial_algorithmic[i]; - } - message << "HHT exhausted 100 corrections without satisfying every endpoint check; " - << "stage=outer, outer_iteration=" << iteration - << ", graph_iteration=" << trial.Value().root.graph_iteration_count - << ", graph_residual=" << trial.Value().root.graph_residual_infinity - << ", graph_relative=" << trial.Value().root.graph_residual_relative - << ", graph_correction=" << trial.Value().root.graph_correction_infinity - << ", max_NtE=" << full_hidden - << ", max_analytic_psi=" << analytic_hidden - << ", max_NtInertia=" << inertia_hidden - << ", max_NtE_minus_psi_minus_NtInertia=" << consistency - << ", max_pp=" << maximum(pp) - << ", max_beta_h2_trial_Ar=" << maximum(position_correction) - << ", max_trial_p=" << maximum(trial_p) - << ", max_vp_predictor=" << maximum(vpp) - << ", max_gamma_h_trial_Ar=" << maximum(velocity_correction) - << ", max_trial_v=" << maximum(trial_v) - << ", max_physical_v=" << maximum(trial.Value().velocity) << "."; - return fail(Failure(domain, "dynamic-nonconvergence", message.str())); - } - status = log.Write(record); - if (!status.IsOk()) return Result::Failure(status); - const double velocity_rate = parameters.gamma / (parameters.beta * h); - const double acceleration_rate = 1.0 / ((1.0 + parameters.alpha) * parameters.beta * h * h); - auto system = AssembleAugmentedReducedSystem(trial.Value().assembly, chart.Value(), operators.Value(), velocity_rate, acceleration_rate, length, h); - if (!system.HasValue()) return fail(system.GetStatus()); - const bool angular_only = ShouldRefineAngular( - chart.Value(), candidate.free_residual_metrics, - {record.coordinate_correction[0U], record.velocity_correction[0U], - record.acceleration_correction[0U], record.algorithmic_correction[0U]}, - translation_private_algorithmic_pass); - auto selected = SelectAugmentedSolve(system.Value(), chart.Value(), angular_only); - if (!selected.HasValue()) return fail(selected.GetStatus()); - const auto linear_failure = [&](const Status& failure) { - return fail(failure, angular_only ? "angular-newton-linear-solve" - : "full-newton-linear-solve"); - }; - Vector selected_solution{selected.Value().rhs.Size()}; - if (selected_solution.Size() > 0U) { - status = tangent_solver.FactorizeFresh(selected.Value().tangent); - if (!status.IsOk()) return linear_failure(status); - status = tangent_solver.SolveCorrection(selected.Value().rhs, selected_solution); - if (!status.IsOk()) return linear_failure(status); - status = CheckLinearSolve(selected.Value().tangent, selected.Value().rhs, selected_solution); - if (!status.IsOk()) return linear_failure(status); - } - auto solution = EmbedAugmentedSolution(selected.Value(), selected_solution, - system.Value().rhs.Size()); - if (!solution.HasValue()) return fail(solution.GetStatus()); - auto correction = MapAugmentedCorrection(chart.Value(), system.Value(), solution.Value(), velocity_rate, acceleration_rate); - if (!correction.HasValue()) return fail(correction.GetStatus()); - Vector full_algorithmic{dofs.FullDofCount()}; - Vector reduced_algorithmic{correction.Value().p.Size()}; - Vector reduced_physical{correction.Value().p.Size()}; - for (std::size_t i = 0U; i < full_algorithmic.Size(); ++i) - full_algorithmic[i] = - (1.0 + parameters.alpha) * correction.Value().acceleration[i]; - for (std::size_t i = 0U; i < reduced_algorithmic.Size(); ++i) { - reduced_algorithmic[i] = correction.Value().p[i] / - (parameters.beta * h * h); - reduced_physical[i] = acceleration_rate * correction.Value().p[i]; - } - CorrectionRecord(correction.Value().coordinate, correction.Value().velocity, - correction.Value().acceleration, full_algorithmic, - reduced_algorithmic, reduced_physical, chart.Value(), dofs, length, h, - parameters.alpha, - &record); - translation_private_algorithmic_pass = true; - for (std::size_t i = 0U; i < chart.Value().p_dofs.size(); ++i) - if (PhysicalCoordinateChannel(chart.Value(), i) == 0U) - translation_private_algorithmic_pass = - translation_private_algorithmic_pass && - PrivateCorrectionEqual(chart.Value(), i, full_algorithmic, - reduced_algorithmic, reduced_physical, parameters.alpha); - copy_translation_rates = angular_only; - if (angular_only) { - retained_velocity = trial_v; - retained_acceleration = trial_a; - } - for (std::size_t i = 0U; i < trial_algorithmic.Size(); ++i) { - if (angular_only && PhysicalCoordinateChannel(chart.Value(), i) == 0U) continue; - trial_algorithmic[i] += reduced_algorithmic[i]; - } - for (std::size_t i = 0U; i < trial_p.Size(); ++i) { - if (angular_only && PhysicalCoordinateChannel(chart.Value(), i) == 0U) continue; - AdvanceCoordinate(correction.Value().p[i], &trial_p[i], &coordinate_low[i]); - } - for (std::size_t i = 0U; i < increment_trace.Size(); ++i) { - if (angular_only && PhysicalCoordinateChannel(chart.Value(), i) == 0U) continue; - AddIncrementTraceTerm(correction.Value().p[i], &increment_trace[i], - &increment_trace_compensation[i]); - increment_trace_absolute_sum[i] += std::abs(correction.Value().p[i]); - } - seed = trial.Value().root.z; - for (std::size_t i = 0U; i < seed.Size(); ++i) - seed[i] += correction.Value().z[i]; - } - if (!accepted) return Result::Failure( - Failure(domain, "dynamic-nonconvergence", "Dynamic increment was not accepted.")); + const auto report_failure = [&](const Status& failure, const char* stage) { + const Status contextual = WithProcedureContext( + domain, failure, stage, increment, record.time); + record.stage = stage; + record.outcome = "failed"; + record.failure_code = contextual.Diagnostics().empty() + ? "dynamic-solver-failure" + : contextual.Diagnostics().front().code; + const Status written = log.Write(record); + return written.IsOk() ? contextual : written; + }; + status = RunReducedHhtStep( + integrator, history, h, parameters, + [&](const ShellHhtIntegrator& step_integrator, + ReducedShellPredictor predictor) -> Result { + Vector pp = std::move(predictor.pp); + Vector vpp = std::move(predictor.vpp); + DynamicAcceptedFrameState trial_frame = old; + trial_frame.is_initial = false; + trial_frame.frame_index = increment; + trial_frame.increment_index = increment; + trial_frame.time = static_cast(increment) * h; + trial_frame.time_integration_formulation = + DynamicTimeIntegrationFormulation::kReducedCoordinateHht; + Vector trial_algorithmic{history.p.Size()}; + Vector trial_p = pp; + Vector coordinate_low = std::move(predictor.coordinate_low); + Vector chosen_start_p = pp; + Vector increment_trace = std::move(predictor.increment_trace); + Vector increment_trace_compensation = + std::move(predictor.increment_trace_compensation); + Vector increment_trace_absolute_sum = + std::move(predictor.increment_trace_absolute_sum); + Vector seed = history.z; + record = {}; + record.increment_index = increment; + record.time = trial_frame.time; + record.correction_pass = true; + bool translation_private_algorithmic_pass = true; + bool copy_translation_rates = false; + Vector retained_velocity{history.p.Size()}; + Vector retained_acceleration{history.p.Size()}; + using StepResult = Result>; + const auto write_iteration = + [&](const DynamicNewtonIterationLogRecord& value) { + return log.Write(value); + }; + ReducedShellHhtStep iteration_step( + [&](std::uint32_t iteration) -> StepResult { + record.iteration_index = iteration; + record.stage = "evaluation"; + record.outcome = "candidate"; + const auto fail = [&](const Status& failure, + const char* stage = "outer") -> StepResult { + return StepResult::Failure(report_failure(failure, stage)); + }; + ReducedShellTrialRates reduced_rates{ + Vector{history.p.Size()}, Vector{history.p.Size()}}; + step_integrator.EvaluateRates( + {chart.Value(), history, trial_algorithmic, vpp, + copy_translation_rates, retained_velocity, retained_acceleration, + h, parameters}, reduced_rates.velocity, + reduced_rates.acceleration); + Vector trial_v = std::move(reduced_rates.velocity); + Vector trial_a = std::move(reduced_rates.acceleration); + const auto evaluate_trial = + [&](const Vector& physical_p, const Vector& physical_v, + const Vector& physical_a, + const Vector& root_seed) -> Result { + auto root = SolveEmbeddingRoot( + chart.Value(), physical_p, root_seed, + [&](const Vector& q) { + return position_operators(trial_frame, q); + }, + tangent_solver); + if (!root.HasValue()) + return Result::Failure(WithProcedureContext( + domain, root.GetStatus(), "inner-position", increment, + trial_frame.time)); + auto rates = step_integrator.ReconstructPhysicalRates( + chart.Value(), root.Value(), physical_v, physical_a, + tangent_solver); + if (!rates.HasValue()) + return Result::Failure( + WithProcedureContext(domain, rates.GetStatus(), "inner-rates", + increment, trial_frame.time)); + auto candidate = trial_frame; + candidate.generalized_coordinates = root.Value().q; + candidate.velocity = rates.Value().velocity; + candidate.acceleration = rates.Value().acceleration; + candidate.algorithmic_acceleration = Vector(dofs.FullDofCount()); + for (std::size_t i = 0U; + i < candidate.algorithmic_acceleration.Size(); ++i) + candidate.algorithmic_acceleration[i] = + (1.0 + parameters.alpha) * candidate.acceleration[i] - + parameters.alpha * old.acceleration[i]; + candidate.iteration_count = iteration; + auto assembly = EvaluateAssembly(domain, dofs, shells.Value(), + candidate, loads.Value()[1U], h, + parameters, parallel_for, true); + if (!assembly.HasValue()) + return Result::Failure( + WithProcedureContext(domain, assembly.GetStatus(), "outer", + increment, trial_frame.time)); + assembly.Value().frame.time_integration_formulation = + DynamicTimeIntegrationFormulation::kReducedCoordinateHht; + return Result::Success( + {std::move(assembly.Value()), std::move(root.Value()), + physical_p, physical_v, physical_a, trial_algorithmic, + std::move(rates.Value().z_velocity), + std::move(rates.Value().z_acceleration)}); + }; + position_geometry_failure = false; + auto trial = evaluate_trial(trial_p, trial_v, trial_a, seed); + if (!trial.HasValue() && iteration == 0U && + position_geometry_failure) { + for (std::size_t i = 0U; i < trial_p.Size(); ++i) { + trial_algorithmic[i] = + (history.p[i] - pp[i]) / (parameters.beta * h * h); + trial_p[i] = history.p[i]; + chosen_start_p[i] = history.p[i]; + coordinate_low[i] = 0.0; + increment_trace[i] = 0.0; + increment_trace_compensation[i] = 0.0; + increment_trace_absolute_sum[i] = 0.0; + } + step_integrator.EvaluateRates( + {chart.Value(), history, trial_algorithmic, vpp, false, + retained_velocity, retained_acceleration, h, parameters}, + trial_v, trial_a); + trial = evaluate_trial(trial_p, trial_v, trial_a, seed); + } + if (!trial.HasValue()) + return fail(trial.GetStatus()); + if (iteration == 0U && !ExactlyEqual(trial_p, history.p)) { + Vector old_algorithmic{trial_p.Size()}; + Vector old_velocity{trial_p.Size()}; + Vector old_acceleration{trial_p.Size()}; + for (std::size_t i = 0U; i < trial_p.Size(); ++i) { + old_algorithmic[i] = + (history.p[i] - pp[i]) / (parameters.beta * h * h); + } + step_integrator.EvaluateRates( + {chart.Value(), history, old_algorithmic, vpp, false, + retained_velocity, retained_acceleration, h, parameters}, + old_velocity, old_acceleration); + auto old_trial = + evaluate_trial(history.p, old_velocity, old_acceleration, seed); + if (!old_trial.HasValue()) + return fail(old_trial.GetStatus()); + const auto reduced_residual = [&](const Trial& value) { + double norm = 0.0; + for (const auto full : dofs.FreeDofs()) { + const double scale = full % 6U < 3U ? length : 1.0; + norm = (std::max)(norm, + scale * + std::abs( + value.assembly.frame.residual[full])); + } + if (!std::isfinite(norm)) + return std::numeric_limits::infinity(); + return norm; + }; + const double old_residual = reduced_residual(old_trial.Value()); + const double predictor_residual = reduced_residual(trial.Value()); + if (!std::isfinite(old_residual) || + !std::isfinite(predictor_residual)) + return fail( + Failure(domain, "dynamic-invalid-state", + "The scaled initial reduced residual is nonfinite.")); + if (old_residual < predictor_residual) { + trial = std::move(old_trial); + trial_p = history.p; + chosen_start_p = history.p; + coordinate_low = Vector(history.p.Size()); + increment_trace = Vector(history.p.Size()); + increment_trace_compensation = Vector(history.p.Size()); + increment_trace_absolute_sum = Vector(history.p.Size()); + trial_v = old_velocity; + trial_a = old_acceleration; + trial_algorithmic = old_algorithmic; + } + } + auto candidate = trial.Value().assembly.frame; + auto operators = EvaluateConstraintOperators( + domain, dofs, shells.Value(), trial.Value().assembly, + chart.Value(), zero, zero); + if (!operators.HasValue()) + return fail(operators.GetStatus()); + ResidualRecord(dofs, candidate, &record); + if (record.residual_pass && record.correction_pass) { + status = CompleteConstraintEvidence( + domain, dofs, trial.Value().assembly, length, null_nodes, + positive_solver, tangent_solver, &candidate); + if (!status.IsOk()) + return fail(status, "accepted"); + status = CompleteEquilibrium(domain, dofs, trial.Value().assembly, + &candidate); + if (!status.IsOk()) + return fail(status, "accepted"); + graph_regularity = + CheckGraphRegularity(trial.Value().assembly, chart.Value(), + operators.Value(), tangent_solver); + if (!graph_regularity.HasValue()) + return fail(graph_regularity.GetStatus(), "accepted"); + status = CheckReducedUpdates( + domain, old, candidate, pp, vpp, trial_p, trial_v, trial_a, + trial.Value().root.z, trial.Value().algebraic_velocity, + trial.Value().algebraic_acceleration, trial_algorithmic, + history.p, history.velocity, history.acceleration, + history.algorithmic_acceleration, increment_trace, + increment_trace_compensation, increment_trace_absolute_sum, + chart.Value(), chosen_start_p, coordinate_low, parameters, h); + if (!status.IsOk()) + return fail(status, "accepted"); + candidate.external_work = 0.0; + for (std::size_t i = 0U; i < dofs.FullDofCount(); ++i) + candidate.external_work += candidate.external_force[i] * + candidate.generalized_coordinates[i]; + candidate.energy_defect = candidate.kinetic_energy + + candidate.physical_strain_energy + + candidate.drilling_energy - + initial_energy - candidate.external_work; + return StepResult::Success(ReducedStepCandidate{ + std::move(candidate), + {trial_p, trial_v, trial_a, trial_algorithmic, + trial.Value().root.z}}); + } + if (!CanApplyOuterCorrection(iteration)) { + double full_hidden = 0.0; + double analytic_hidden = 0.0; + double inertia_hidden = 0.0; + double consistency = 0.0; + for (std::size_t row = 0U; row < chart.Value().null_nodes.size(); + ++row) { + const auto node = chart.Value().null_nodes[row]; + const auto& null = + trial.Value().assembly.rotations[node].drill_null; + double full = 0.0; + double inertia = 0.0; + for (std::size_t component = 0U; component < 3U; ++component) { + const auto full_dof = 6U * node + 3U + component; + full += null[component] * candidate.residual[full_dof]; + inertia += null[component] * candidate.inertia_force[full_dof]; + } + const double analytic = operators.Value().rows[row].value; + full_hidden = (std::max)(full_hidden, std::abs(full)); + analytic_hidden = (std::max)(analytic_hidden, std::abs(analytic)); + inertia_hidden = (std::max)(inertia_hidden, std::abs(inertia)); + consistency = + (std::max)(consistency, std::abs(full - analytic - inertia)); + } + std::ostringstream message; + const auto maximum = [](const Vector& values) { + double result = 0.0; + for (std::size_t i = 0U; i < values.Size(); ++i) + result = (std::max)(result, std::abs(values[i])); + return result; + }; + Vector position_correction{trial_algorithmic.Size()}; + Vector velocity_correction{trial_algorithmic.Size()}; + for (std::size_t i = 0U; i < trial_algorithmic.Size(); ++i) { + position_correction[i] = + parameters.beta * h * h * trial_algorithmic[i]; + velocity_correction[i] = + parameters.gamma * h * trial_algorithmic[i]; + } + message << "HHT exhausted 100 corrections without satisfying every " + "endpoint check; " + << "stage=outer, outer_iteration=" << iteration + << ", graph_iteration=" + << trial.Value().root.graph_iteration_count + << ", graph_residual=" + << trial.Value().root.graph_residual_infinity + << ", graph_relative=" + << trial.Value().root.graph_residual_relative + << ", graph_correction=" + << trial.Value().root.graph_correction_infinity + << ", max_NtE=" << full_hidden + << ", max_analytic_psi=" << analytic_hidden + << ", max_NtInertia=" << inertia_hidden + << ", max_NtE_minus_psi_minus_NtInertia=" << consistency + << ", max_pp=" << maximum(pp) + << ", max_beta_h2_trial_Ar=" << maximum(position_correction) + << ", max_trial_p=" << maximum(trial_p) + << ", max_vp_predictor=" << maximum(vpp) + << ", max_gamma_h_trial_Ar=" << maximum(velocity_correction) + << ", max_trial_v=" << maximum(trial_v) + << ", max_physical_v=" << maximum(trial.Value().velocity) + << "."; + return fail( + Failure(domain, "dynamic-nonconvergence", message.str())); + } + status = write_iteration(record); + if (!status.IsOk()) + return StepResult::Failure(status); + const double velocity_rate = parameters.gamma / (parameters.beta * h); + const double acceleration_rate = + 1.0 / ((1.0 + parameters.alpha) * parameters.beta * h * h); + auto system = AssembleAugmentedReducedSystem( + trial.Value().assembly, chart.Value(), operators.Value(), + velocity_rate, acceleration_rate, length, h); + if (!system.HasValue()) + return fail(system.GetStatus()); + const bool angular_only = ShouldRefineAngular( + chart.Value(), candidate.free_residual_metrics, + {record.coordinate_correction[0U], record.velocity_correction[0U], + record.acceleration_correction[0U], + record.algorithmic_correction[0U]}, + translation_private_algorithmic_pass); + auto selected = + SelectAugmentedSolve(system.Value(), chart.Value(), angular_only); + if (!selected.HasValue()) + return fail(selected.GetStatus()); + const auto linear_failure = [&](const Status& failure) { + return fail(failure, angular_only ? "angular-newton-linear-solve" + : "full-newton-linear-solve"); + }; + Vector selected_solution{selected.Value().rhs.Size()}; + if (selected_solution.Size() > 0U) { + status = tangent_solver.FactorizeFresh(selected.Value().tangent); + if (!status.IsOk()) + return linear_failure(status); + status = tangent_solver.SolveCorrection(selected.Value().rhs, + selected_solution); + if (!status.IsOk()) + return linear_failure(status); + status = CheckLinearSolve(selected.Value().tangent, + selected.Value().rhs, selected_solution); + if (!status.IsOk()) + return linear_failure(status); + } + auto solution = EmbedAugmentedSolution( + selected.Value(), selected_solution, system.Value().rhs.Size()); + if (!solution.HasValue()) + return fail(solution.GetStatus()); + auto correction = MapAugmentedCorrection( + chart.Value(), system.Value(), solution.Value(), velocity_rate, + acceleration_rate); + if (!correction.HasValue()) + return fail(correction.GetStatus()); + Vector full_algorithmic{dofs.FullDofCount()}; + Vector reduced_algorithmic{correction.Value().p.Size()}; + Vector reduced_physical{correction.Value().p.Size()}; + for (std::size_t i = 0U; i < full_algorithmic.Size(); ++i) + full_algorithmic[i] = + (1.0 + parameters.alpha) * correction.Value().acceleration[i]; + for (std::size_t i = 0U; i < reduced_algorithmic.Size(); ++i) { + reduced_algorithmic[i] = + correction.Value().p[i] / (parameters.beta * h * h); + reduced_physical[i] = acceleration_rate * correction.Value().p[i]; + } + CorrectionRecord(correction.Value().coordinate, + correction.Value().velocity, + correction.Value().acceleration, full_algorithmic, + reduced_algorithmic, reduced_physical, chart.Value(), + dofs, length, h, parameters.alpha, &record); + translation_private_algorithmic_pass = true; + for (std::size_t i = 0U; i < chart.Value().p_dofs.size(); ++i) + if (PhysicalCoordinateChannel(chart.Value(), i) == 0U) + translation_private_algorithmic_pass = + translation_private_algorithmic_pass && + PrivateCorrectionEqual(chart.Value(), i, full_algorithmic, + reduced_algorithmic, reduced_physical, + parameters.alpha); + copy_translation_rates = angular_only; + if (angular_only) { + retained_velocity = trial_v; + retained_acceleration = trial_a; + } + for (std::size_t i = 0U; i < trial_algorithmic.Size(); ++i) { + if (angular_only && + PhysicalCoordinateChannel(chart.Value(), i) == 0U) + continue; + trial_algorithmic[i] += reduced_algorithmic[i]; + } + for (std::size_t i = 0U; i < trial_p.Size(); ++i) { + if (angular_only && + PhysicalCoordinateChannel(chart.Value(), i) == 0U) + continue; + AdvanceCoordinate(correction.Value().p[i], &trial_p[i], + &coordinate_low[i]); + } + for (std::size_t i = 0U; i < increment_trace.Size(); ++i) { + if (angular_only && + PhysicalCoordinateChannel(chart.Value(), i) == 0U) + continue; + AddIncrementTraceTerm(correction.Value().p[i], &increment_trace[i], + &increment_trace_compensation[i]); + increment_trace_absolute_sum[i] += + std::abs(correction.Value().p[i]); + } + seed = trial.Value().root.z; + for (std::size_t i = 0U; i < seed.Size(); ++i) + seed[i] += correction.Value().z[i]; + return StepResult::Success(std::nullopt); + }, + kMaximumOuterCorrections); + const Status solved = iteration_step.Solve(algorithm); + if (!solved.IsOk()) + return Result::Failure(solved); + if (!iteration_step.HasCandidate()) return Result::Failure( + Failure(domain, "dynamic-nonconvergence", "Dynamic increment was not accepted.")); + return Result::Success( + iteration_step.TakeCandidate()); + }, + [&](ReducedStepCandidate&& accepted_candidate) -> Status { + status = state.CommitAcceptedFrame(accepted_candidate.frame); + if (!status.IsOk()) + return report_failure(status, "accepted"); + history = std::move(accepted_candidate.history); + record.stage = "accepted"; + record.outcome = "accepted"; + status = log.Write(record); + if (status.IsOk()) + status = log.Flush(); + return status; + }); + if (!status.IsOk()) return Result::Failure(status); } - return Result::Success({std::move(state), domain.Warnings()}); + return Result::Success( + {std::move(state), domain.Warnings()}); } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/analysis/hht_reduced_procedure.h b/src/fesa/analysis/hht_reduced_procedure.h index 93d72d9..572bfb7 100644 --- a/src/fesa/analysis/hht_reduced_procedure.h +++ b/src/fesa/analysis/hht_reduced_procedure.h @@ -3,16 +3,20 @@ #include -#include "fesa/analysis/prepared_analysis.h" +#include "fesa/analysis/dynamic_execution_result.h" +#include "fesa/core/status.h" namespace fesa { namespace hht_reduced_internal { +class ShellHhtIntegrator; constexpr std::uint32_t kMaximumOuterCorrections = 100U; constexpr bool CanApplyOuterCorrection(const std::uint32_t completed) { return completed < kMaximumOuterCorrections; } } // namespace hht_reduced_internal +class Domain; +class EquilibriumAlgorithm; class LinearSolver; class NewtonIterationLogWriter; class ParallelFor; @@ -22,6 +26,14 @@ Result ExecuteReducedHht( const Domain& domain, NewtonIterationLogWriter& log, const ParallelFor& parallel_for, LinearSolver& positive_solver, TangentSolver& tangent_solver); + +/// @brief Borrows caller iteration policy for the complete production core. +Result ExecuteReducedHht( + const Domain& domain, NewtonIterationLogWriter& log, + const ParallelFor& parallel_for, LinearSolver& positive_solver, + TangentSolver& tangent_solver, + const hht_reduced_internal::ShellHhtIntegrator& integrator, + EquilibriumAlgorithm& algorithm); } // namespace fesa #endif // FESA_ANALYSIS_HHT_REDUCED_PROCEDURE_H_ diff --git a/src/fesa/analysis/linear_frequency_procedure.cpp b/src/fesa/analysis/linear_frequency_procedure.cpp index fe3110c..d351098 100644 --- a/src/fesa/analysis/linear_frequency_procedure.cpp +++ b/src/fesa/analysis/linear_frequency_procedure.cpp @@ -20,6 +20,7 @@ #include "fesa/elements/mitc4_shell.h" #include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" +#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h" namespace fesa { namespace { @@ -84,7 +85,8 @@ double MaximumEdgeLength(const Domain& domain) { return maximum; } -Result ExecuteCandidate(const Domain& domain) { +Result ExecuteCandidate(const Domain& domain, + const EigenSolver& solver) { const FrequencyStepDefinition* frequency = domain.Frequency(); if (frequency == nullptr || domain.Steps().Size() != 1U) { return Result::Failure( @@ -101,6 +103,7 @@ Result ExecuteCandidate(const Domain& domain) { return Result::Failure(model.GetStatus()); const ElementFactory factory; std::vector> owned; + std::vector> references; ElementView view; owned.reserve(model.Value().ActiveElements().size()); view.reserve(model.Value().ActiveElements().size()); @@ -108,13 +111,15 @@ Result ExecuteCandidate(const Domain& domain) { auto element = factory.Create(domain.Elements()[index], domain); if (!element.HasValue()) return Result::Failure(element.GetStatus()); - if (dynamic_cast(element.Value().get()) == nullptr) { + auto reference = factory.BindShellReference(*element.Value()); + if (reference == nullptr) { return Result::Failure(Failure( "unsupported-frequency-element", std::to_string(index), "The frequency procedure supports MITC4 shell elements only.")); } view.emplace_back(*element.Value()); owned.push_back(std::move(element.Value())); + references.push_back(std::move(reference)); } DofManager dofs; const Status dof_status = dofs.Build(model.Value(), view); @@ -158,7 +163,7 @@ Result ExecuteCandidate(const Domain& domain) { std::to_string(definition.MaterialIndex()), "Every active shell material requires a positive density.")); } - const auto* element = dynamic_cast(owned[shell].get()); + const auto& element = references[shell]; auto mass = element->Mass(density[definition.MaterialIndex()]); auto shell_stiffness = element->Stiffness(); if (!mass.HasValue()) @@ -207,7 +212,7 @@ Result ExecuteCandidate(const Domain& domain) { (material->YoungsModulus() / density[material_id]) / length / length); } - auto backend = MklFeastEigenSolver{}.Solve( + auto backend = solver.Solve( reduction.Value().whitened_operator, request, zero_scale); if (!backend.HasValue()) return Result::Failure(backend.GetStatus()); @@ -403,7 +408,8 @@ Result ExecuteCandidate(const Domain& domain) { Result LinearFrequencyProcedureExecutor::Execute( const Domain& domain) const { try { - auto result = ExecuteCandidate(domain); + const MklFeastEigenSolver default_solver; + auto result = ExecuteCandidate(domain, solver_ ? *solver_ : default_solver); if (!result.HasValue()) { auto diagnostics = result.GetStatus().Diagnostics(); for (auto& diagnostic : diagnostics) { diff --git a/src/fesa/analysis/linear_static_procedure.cpp b/src/fesa/analysis/linear_static_procedure.cpp index f107523..8734275 100644 --- a/src/fesa/analysis/linear_static_procedure.cpp +++ b/src/fesa/analysis/linear_static_procedure.cpp @@ -142,7 +142,7 @@ Result LinearStaticProcedureExecutor::Execute( } state.ExternalForce() = std::move(nodal_external.Value()); - Result rhs = LoadAssembler::EffectiveFreeRhs( + Result rhs = EssentialConstraintPolicy{}.EffectiveFreeRhs( state.ExternalForce(), partitions.Value().k_fc, dofs.PrescribedValues(), dofs); if (!rhs.HasValue()) { diff --git a/src/fesa/analysis/nonlinear_analysis_state.cpp b/src/fesa/analysis/nonlinear_analysis_state.cpp index 79c6a91..23c3ee4 100644 --- a/src/fesa/analysis/nonlinear_analysis_state.cpp +++ b/src/fesa/analysis/nonlinear_analysis_state.cpp @@ -1,6 +1,5 @@ #include "fesa/analysis/nonlinear_analysis_state.h" -#include #include #include #include @@ -9,11 +8,18 @@ #include #include +#include "fesa/core/source_identity.h" #include "fesa/elements/element_definition.h" +#include "math/finite_values.h" +#include "results/shell_result_recovery.h" +#include "analysis/shell_state_inventory.h" namespace fesa { namespace { +using analysis_internal::ShellElementOrder; +using math_internal::IsFinite; + constexpr std::size_t kDofsPerNode = 6U; constexpr std::size_t kShellLocationsPerElement = 4U; constexpr std::uint32_t kMaximumNewtonIterations = 100U; @@ -27,59 +33,6 @@ Status StateFailure(const FailureCategory category, const std::string& code, message}}); } -/// @brief Compares every preserved source-identity component. -bool SourceIdsMatch(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; -} - -template -/// @brief Tests one fixed-size numerical array for finite values. -bool IsFinite(const std::array& values) { - return std::all_of(values.begin(), values.end(), - [](const double value) { return std::isfinite(value); }); -} - -/// @brief Tests each component of a stored trial vector for finiteness. -bool IsFinite(const Vector& values) { - for (std::size_t index = 0U; index < values.Size(); ++index) { - if (!std::isfinite(values[index])) { - return false; - } - } - return true; -} - -/// @brief Tests all physical quantities attached to one shell recovery row. -bool IsFinite(const ShellResultRow& row) { - if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) || - !IsFinite(row.section_resultant)) { - return false; - } - for (const auto& axis : row.local_frame) { - if (!IsFinite(axis)) { - return false; - } - } - return std::all_of(row.stress.begin(), row.stress.end(), - [](const ShellSectionStressRow& stress) { - return std::isfinite(stress.zeta) && - IsFinite(stress.components); - }); -} - -/// @brief Returns shell Domain indices in deterministic unified element order. -std::vector ShellElementOrder(const Domain& domain) { - std::vector order; - for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { - if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) { - order.push_back(static_cast(index)); - } - } - return order; -} - /// @brief Validates one accepted frame before any committed-state mutation. Status ValidateAcceptedFrame(const AcceptedFrameState& frame, const NonlinearTrialState& committed, @@ -117,7 +70,7 @@ Status ValidateAcceptedFrame(const AcceptedFrameState& frame, for (std::size_t node = 0U; node < node_count; ++node) { if (frame.nodal_internal_order[node] != committed.nodal_internal_order[node] || - !SourceIdsMatch(frame.nodal_source_ids[node], + !SourceIdentityEquals(frame.nodal_source_ids[node], committed.nodal_source_ids[node]) || !IsFinite(frame.nodal_triad[node][0U]) || !IsFinite(frame.nodal_triad[node][1U]) || @@ -179,7 +132,7 @@ Status ValidateAcceptedFrame(const AcceptedFrameState& frame, if (row.element != shell_order[element] || row.location != expected_locations[point] || row.natural_coordinates != expected_coordinates[point] || - !IsFinite(row)) { + !results_internal::IsFiniteShellRow(row)) { return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {}, "Shell result rows do not preserve stable identity."); } @@ -241,7 +194,7 @@ Result NonlinearAnalysisState::Create( for (std::size_t node = 0U; node < node_count; ++node) { const auto& entry = baseline.node_baselines[node]; if (entry.node_index != static_cast(node) || - !SourceIdsMatch(entry.source_id, domain.Nodes()[node].source_id) || + !SourceIdentityEquals(entry.source_id, domain.Nodes()[node].source_id) || !IsFinite(entry.triad[0U]) || !IsFinite(entry.triad[1U]) || !IsFinite(entry.triad[2U]) || !IsFinite(entry.director)) { return Result::Failure(StateFailure( diff --git a/src/fesa/analysis/nonlinear_static_control.cpp b/src/fesa/analysis/nonlinear_static_control.cpp index 86235ae..b1daa03 100644 --- a/src/fesa/analysis/nonlinear_static_control.cpp +++ b/src/fesa/analysis/nonlinear_static_control.cpp @@ -12,10 +12,17 @@ #include "fesa/analysis/analysis_model.h" #include "fesa/assembly/load_assembler.h" #include "fesa/elements/finite_rotation.h" +#include "math/vector3_operations.h" +#include "elements/shell_kernel_values.h" +#include "math/dense_blas_internal.h" namespace fesa { namespace { +using shell_kernel_internal::FrameMatrix; +using math_internal::Multiply; +using math_internal::Skew; + constexpr std::size_t kDofsPerNode = 6U; constexpr std::size_t kTranslationDofCount = 3U; constexpr double kMomentAdmissibilityThreshold = 1.0e-12; @@ -67,27 +74,6 @@ Result RotationVectorForNode(const Vector& trial, return Result::Success(rotation); } -/// @brief Builds a proper initial frame from its semantic nodal record. -Matrix InitialFrame(const ShellNodeInitialFrame& frame) { - Matrix result{3U, 3U}; - for (std::size_t row = 0U; row < kTranslationDofCount; ++row) { - result(row, 0U) = frame.tangent_a[row]; - result(row, 1U) = frame.tangent_b[row]; - result(row, 2U) = frame.director[row]; - } - return result; -} - -/// @brief Multiplies a 3-by-3 matrix by a Vector3. -Vector3 Multiply(const Matrix& matrix, const Vector3& vector) { - return {matrix(0U, 0U) * vector.X() + matrix(0U, 1U) * vector.Y() + - matrix(0U, 2U) * vector.Z(), - matrix(1U, 0U) * vector.X() + matrix(1U, 1U) * vector.Y() + - matrix(1U, 2U) * vector.Z(), - matrix(2U, 0U) * vector.X() + matrix(2U, 1U) * vector.Y() + - matrix(2U, 2U) * vector.Z()}; -} - /// @brief Multiplies a 3-by-3 matrix transpose by a Vector3. Vector3 MultiplyTranspose(const Matrix& matrix, const Vector3& vector) { return {matrix(0U, 0U) * vector.X() + matrix(1U, 0U) * vector.Y() + @@ -98,37 +84,18 @@ Vector3 MultiplyTranspose(const Matrix& matrix, const Vector3& vector) { matrix(2U, 2U) * vector.Z()}; } -/// @brief Forms the three-dimensional skew matrix of a global vector. -Matrix Skew(const Vector3& vector) { - Matrix result{3U, 3U}; - result(0U, 1U) = -vector.Z(); - result(0U, 2U) = vector.Y(); - result(1U, 0U) = vector.Z(); - result(1U, 2U) = -vector.X(); - result(2U, 0U) = -vector.Y(); - result(2U, 1U) = vector.X(); - return result; -} - /// @brief Scales a small dense matrix without changing its shape. Matrix Scale(const Matrix& matrix, const double scale) { - Matrix result{matrix.Rows(), matrix.Columns()}; - for (std::size_t row = 0U; row < matrix.Rows(); ++row) { - for (std::size_t column = 0U; column < matrix.Columns(); ++column) { - result(row, column) = scale * matrix(row, column); - } - } + Matrix result{matrix}; + result.Scale(scale); return result; } /// @brief Adds matching small dense matrices. Matrix Add(const Matrix& left, const Matrix& right) { - Matrix result{left.Rows(), left.Columns()}; - for (std::size_t row = 0U; row < left.Rows(); ++row) { - for (std::size_t column = 0U; column < left.Columns(); ++column) { - result(row, column) = left(row, column) + right(row, column); - } - } + Matrix result{left}; + dense_blas_internal::AxpyValues(1.0, right.Data(), + left.Rows() * left.Columns(), result.Data()); return result; } @@ -472,7 +439,7 @@ Result AssembleGlobalFixedLoadContribution( return Result::Failure(phi.GetStatus()); } auto rotation = - MakeFiniteRotationState(phi.Value(), InitialFrame(*frames[node])); + MakeFiniteRotationState(phi.Value(), FrameMatrix(*frames[node])); if (!rotation.HasValue()) { return Result::Failure(rotation.GetStatus()); } diff --git a/src/fesa/analysis/nonlinear_static_core.h b/src/fesa/analysis/nonlinear_static_core.h new file mode 100644 index 0000000..25298f8 --- /dev/null +++ b/src/fesa/analysis/nonlinear_static_core.h @@ -0,0 +1,16 @@ +#ifndef FESA_ANALYSIS_NONLINEAR_STATIC_CORE_H_ +#define FESA_ANALYSIS_NONLINEAR_STATIC_CORE_H_ +#include "fesa/analysis/nonlinear_static_execution_result.h" +namespace fesa { +class EquilibriumAlgorithm; +class NewtonIterationLogWriter; +class ParallelFor; +class TangentSolver; +/// @brief Runs the production core with caller-owned iteration policy. +/// @note All adapters outlive this synchronous call; candidate/history stay local. +Result ExecuteNonlinearStaticCore( + const Domain& domain, NewtonIterationLogWriter& log, + const ParallelFor& parallel_for, TangentSolver& tangent_solver, + EquilibriumAlgorithm& algorithm); +} // namespace fesa +#endif // FESA_ANALYSIS_NONLINEAR_STATIC_CORE_H_ diff --git a/src/fesa/analysis/nonlinear_static_procedure.cpp b/src/fesa/analysis/nonlinear_static_procedure.cpp index c503758..4412f22 100644 --- a/src/fesa/analysis/nonlinear_static_procedure.cpp +++ b/src/fesa/analysis/nonlinear_static_procedure.cpp @@ -1,11 +1,15 @@ #include "fesa/analysis/nonlinear_static_procedure.h" +#include "equilibrium_algorithm.h" +#include "analysis/nonlinear_static_core.h" + #include #include #include #include #include #include +#include #include #include #include @@ -17,6 +21,7 @@ #include "fesa/assembly/parallel_for.h" #include "fesa/assembly/surface_pressure_assembler.h" #include "fesa/constraints/essential_constraint_policy.h" +#include "fesa/elements/element_capabilities.h" #include "fesa/elements/finite_rotation.h" #include "fesa/elements/mitc4_shell.h" #include "fesa/elements/nonlinear_mitc4_shell.h" @@ -25,9 +30,16 @@ #include "fesa/properties/shell_section.h" #include "fesa/solvers/linear/tangent_solver.h" +#include "assembly/ordered_contribution_internal.h" +#include "math/finite_values.h" +#include "elements/shell_kernel_values.h" + namespace fesa { namespace { +using shell_kernel_internal::FrameMatrix; +using math_internal::IsFinite; + constexpr std::size_t kDofsPerNode = 6U; constexpr std::size_t kTranslationsPerNode = 3U; constexpr std::size_t kShellDofCount = 24U; @@ -36,7 +48,8 @@ constexpr std::uint32_t kMaximumNewtonIterations = 100U; struct RuntimeShell { EntityIndex element_index; std::array scatter; - NonlinearMitc4Shell element; + std::unique_ptr kernel; + std::unique_ptr element; }; struct InternalAssembly { @@ -124,16 +137,6 @@ Status RecordFailure(NewtonIterationLogWriter& log, return log_status.IsOk() ? failure : log_status; } -Matrix InitialFrame(const ShellNodeInitialFrame& frame) { - Matrix result{3U, 3U}; - for (std::size_t row = 0U; row < kTranslationsPerNode; ++row) { - result(row, 0U) = frame.tangent_a[row]; - result(row, 1U) = frame.tangent_b[row]; - result(row, 2U) = frame.director[row]; - } - return result; -} - Result BuildDofManager(const Domain& domain) { Result model = AnalysisModel::Create(domain); if (!model.HasValue()) { @@ -239,9 +242,20 @@ Result> BuildRuntimeShells(const Domain& domain, return Result>::Failure(shell.GetStatus()); } const EntityIndex element_index = static_cast(index); + auto kernel = + std::make_unique(std::move(shell.Value())); + auto element = std::make_unique( + ElementDofLayout{definition->SourceId(), definition->NodeIndices(), + {DofComponent::kUx, DofComponent::kUy, DofComponent::kUz, + DofComponent::kUrx, DofComponent::kUry, DofComponent::kUrz}}, + *kernel); + auto mapped = dofs.ElementScatter(element->DofLayout()); + if (!mapped.HasValue()) + return Result>::Failure(mapped.GetStatus()); + std::array scatter{}; + std::copy(mapped.Value().begin(), mapped.Value().end(), scatter.begin()); shells.push_back( - {element_index, dofs.ShellElementScatter(element_index), - std::move(shell.Value())}); + {element_index, scatter, std::move(kernel), std::move(element)}); } return Result>::Success(std::move(shells)); } @@ -269,7 +283,7 @@ Status PopulateNodalKinematics(const Domain& domain, trial->generalized_coordinates[first + 4U], trial->generalized_coordinates[first + 5U]}; Result rotation = MakeFiniteRotationState( - phi, InitialFrame(domain.ShellNodeInitialFrames()[node])); + phi, FrameMatrix(domain.ShellNodeInitialFrames()[node])); if (!rotation.HasValue()) { return rotation.GetStatus(); } @@ -304,26 +318,6 @@ NonlinearMitc4TrialDofs ExtractElementTrial( return trial; } -bool IsFinite(const Vector& values) { - for (std::size_t index = 0U; index < values.Size(); ++index) { - if (!std::isfinite(values[index])) { - return false; - } - } - return true; -} - -bool IsFinite(const Matrix& values) { - for (std::size_t row = 0U; row < values.Rows(); ++row) { - for (std::size_t column = 0U; column < values.Columns(); ++column) { - if (!std::isfinite(values(row, column))) { - return false; - } - } - } - return true; -} - Result AssembleInternal( const std::vector& shells, const DofManager& dofs, const Vector& coordinates, const ParallelFor& parallel_for) { @@ -332,9 +326,8 @@ Result AssembleInternal( std::vector> failures(shells.size()); parallel_for.Execute(shells.size(), [&](const std::size_t index) { Result result = - EvaluateInternalResidualAndTangent(shells[index].element, - ExtractElementTrial(shells[index], - coordinates)); + shells[index].element->EvaluateTrial( + ExtractElementTrial(shells[index], coordinates)); if (!result.HasValue()) { failures[index] = result.GetStatus(); return; @@ -363,12 +356,9 @@ Result AssembleInternal( } for (std::size_t row = 0U; row < kShellDofCount; ++row) { force[shells[index].scatter[row]] += local[index]->internal_residual[row]; - for (std::size_t column = 0U; column < kShellDofCount; ++column) { - contributions.push_back( - {shells[index].scatter[row], shells[index].scatter[column], - local[index]->internal_tangent(row, column), index, - row * kShellDofCount + column}); - } + assembly_internal::AppendLocalMatrixRow( + local[index]->internal_tangent, shells[index].scatter, row, index, + contributions); } for (const NonlinearMitc4RecoveryPoint& point : local[index]->recovery.gauss_points) { @@ -573,19 +563,6 @@ Vector CurrentIncrementComponents(const Vector& current, const Vector& start, return SelectFreeComponents(difference, dofs, rotations); } -Vector3 SpatialMoment(const Matrix& transform, - const std::array& generalized) { - return {transform(0U, 0U) * generalized[0U] + - transform(0U, 1U) * generalized[1U] + - transform(0U, 2U) * generalized[2U], - transform(1U, 0U) * generalized[0U] + - transform(1U, 1U) * generalized[1U] + - transform(1U, 2U) * generalized[2U], - transform(2U, 0U) * generalized[0U] + - transform(2U, 1U) * generalized[1U] + - transform(2U, 2U) * generalized[2U]}; -} - /// @brief Computes accepted-state applied-plus-reaction balance evidence. Result> GlobalEquilibrium( const Domain& domain, const DofManager& dofs, @@ -634,7 +611,6 @@ Result> GlobalEquilibrium( constrained[first] ? trial.residual[first] : 0.0, constrained[first + 1U] ? trial.residual[first + 1U] : 0.0, constrained[first + 2U] ? trial.residual[first + 2U] : 0.0}; - const Matrix transform = InverseTransposeLeftJacobianSO3(phi); const std::array applied_generalized{ trial.external_force[first + 3U], trial.external_force[first + 4U], trial.external_force[first + 5U]}; @@ -642,9 +618,16 @@ Result> GlobalEquilibrium( constrained[first + 3U] ? trial.residual[first + 3U] : 0.0, constrained[first + 4U] ? trial.residual[first + 4U] : 0.0, constrained[first + 5U] ? trial.residual[first + 5U] : 0.0}; - const Vector3 applied_moment = SpatialMoment(transform, applied_generalized); - const Vector3 reaction_moment = - SpatialMoment(transform, reaction_generalized); + const auto applied = SpatialMomentSO3(phi, Vector3(applied_generalized)); + const auto reaction = SpatialMomentSO3(phi, Vector3(reaction_generalized)); + if (!applied.HasValue() || !reaction.HasValue()) + return Result>::Failure(ProcedureFailure( + FailureCategory::kSolver, "nonfinite-newton-data", + {domain.SourcePath(), 0U}, "NONLINEAR_STATIC_PROCEDURE", + domain.SourceContentIdentity(), + "Nonlinear global equilibrium balance is nonfinite.")); + const Vector3 applied_moment = applied.Value(); + const Vector3 reaction_moment = reaction.Value(); applied_force_resultant = applied_force_resultant + applied_force; applied_moment_resultant = applied_moment_resultant + position.Cross(applied_force) + applied_moment; @@ -694,11 +677,11 @@ Status ApplyIncrementConstraints(const StepDefinition& step, return Status::Ok(); } -AcceptedFrameState AcceptedFrame(const NonlinearAnalysisState& state, +AcceptedFrameState AcceptedFrame(const std::uint32_t frame_index, const NonlinearTrialState& trial, const LoadFactorIncrement& increment, const std::uint32_t iteration) { - return {static_cast(state.AcceptedFrames().size()), + return {frame_index, increment.increment_index, increment.load_factor, iteration, @@ -728,6 +711,15 @@ NonlinearStaticProcedureExecutor::NonlinearStaticProcedureExecutor( Result NonlinearStaticProcedureExecutor::Execute( const Domain& domain, NewtonIterationLogWriter& log) const { + NewtonStepAlgorithm algorithm; + return ExecuteNonlinearStaticCore(domain, log, parallel_for_, tangent_solver_, + algorithm); +} + +Result ExecuteNonlinearStaticCore( + const Domain& domain, NewtonIterationLogWriter& log, + const ParallelFor& parallel_for, TangentSolver& tangent_solver, + EquilibriumAlgorithm& algorithm) { if (domain.Steps().Size() != 1U || !domain.Steps()[0U].IsNonlinearStatic()) { return Result::Failure(ProcedureFailure( FailureCategory::kInput, "invalid-nonlinear-static-data", @@ -782,199 +774,237 @@ NonlinearStaticProcedureExecutor::Execute( return Result::Failure(constraint_status); } - for (std::uint32_t iteration = 1U; iteration <= kMaximumNewtonIterations; - ++iteration) { - NewtonIterationLogRecord record = - NewLogRecord(step, increment, iteration); - trial.Value().increment_index = increment.increment_index; - trial.Value().load_factor = increment.load_factor; - trial.Value().is_factor_zero_baseline = false; + struct ShellStepCandidate { + AcceptedFrameState frame; + NewtonIterationLogRecord record; + }; + const auto next_frame_index = + static_cast(state.Value().AcceptedFrames().size()); + const auto report_failure = [&](NewtonIterationLogRecord* record, + const Status& failure, + NewtonIterationStage stage) { + return RecordFailure(log, record, failure, stage); + }; + const auto write_iteration = [&](const NewtonIterationLogRecord& record) { + return WriteAndFlush(log, record); + }; + ShellFullNewtonStep iteration_step( + [&](std::uint32_t iteration) + -> Result> { + NewtonIterationLogRecord record = + NewLogRecord(step, increment, iteration); + trial.Value().increment_index = increment.increment_index; + trial.Value().load_factor = increment.load_factor; + trial.Value().is_factor_zero_baseline = false; - Status kinematics = PopulateNodalKinematics(domain, &trial.Value()); - if (!kinematics.IsOk()) { - return Result::Failure( - RecordFailure(log, &record, kinematics, - NewtonIterationStage::kKinematicsEvaluated)); - } - record.last_completed_stage = NewtonIterationStage::kKinematicsEvaluated; + Status kinematics = PopulateNodalKinematics(domain, &trial.Value()); + if (!kinematics.IsOk()) { + return Result>::Failure( + report_failure(&record, kinematics, + NewtonIterationStage::kKinematicsEvaluated)); + } + record.last_completed_stage = + NewtonIterationStage::kKinematicsEvaluated; - Result internal = AssembleInternal( - shells.Value(), dofs.Value(), trial.Value().generalized_coordinates, - parallel_for_); - if (!internal.HasValue()) { - return Result::Failure(RecordFailure( - log, &record, internal.GetStatus(), - NewtonIterationStage::kInternalAssembled)); - } - record.last_completed_stage = NewtonIterationStage::kInternalAssembled; + Result internal = AssembleInternal( + shells.Value(), dofs.Value(), + trial.Value().generalized_coordinates, parallel_for); + if (!internal.HasValue()) { + return Result>::Failure( + report_failure(&record, internal.GetStatus(), + NewtonIterationStage::kInternalAssembled)); + } + record.last_completed_stage = + NewtonIterationStage::kInternalAssembled; - Result fixed = - AssembleGlobalFixedLoadContribution( - domain, dofs.Value(), trial.Value().generalized_coordinates, - increment.load_factor); - if (!fixed.HasValue()) { - return Result::Failure( - RecordFailure(log, &record, fixed.GetStatus(), - NewtonIterationStage::kExternalAssembled)); - } - Result pressure = - AssembleSurfacePressure( - analysis_model.Value(), dofs.Value(), - trial.Value().generalized_coordinates, increment.load_factor, - SurfacePressureConfiguration::kCurrent, parallel_for_); - if (!pressure.HasValue()) { - return Result::Failure( - RecordFailure(log, &record, pressure.GetStatus(), - NewtonIterationStage::kExternalAssembled)); - } - Result external = - CombineExternalContributions(fixed.Value(), pressure.Value(), - dofs.Value()); - if (!external.HasValue()) { - return Result::Failure( - RecordFailure(log, &record, external.GetStatus(), - NewtonIterationStage::kExternalAssembled)); - } - record.last_completed_stage = NewtonIterationStage::kExternalAssembled; + Result fixed = + AssembleGlobalFixedLoadContribution( + domain, dofs.Value(), trial.Value().generalized_coordinates, + increment.load_factor); + if (!fixed.HasValue()) { + return Result>::Failure( + report_failure(&record, fixed.GetStatus(), + NewtonIterationStage::kExternalAssembled)); + } + Result pressure = + AssembleSurfacePressure( + analysis_model.Value(), dofs.Value(), + trial.Value().generalized_coordinates, increment.load_factor, + SurfacePressureConfiguration::kCurrent, parallel_for); + if (!pressure.HasValue()) { + return Result>::Failure( + report_failure(&record, pressure.GetStatus(), + NewtonIterationStage::kExternalAssembled)); + } + Result external = + CombineExternalContributions(fixed.Value(), pressure.Value(), + dofs.Value()); + if (!external.HasValue()) { + return Result>::Failure( + report_failure(&record, external.GetStatus(), + NewtonIterationStage::kExternalAssembled)); + } + record.last_completed_stage = + NewtonIterationStage::kExternalAssembled; - Result residual = - Difference(internal.Value().force, - external.Value().generalized_external_force); - if (!residual.HasValue()) { - return Result::Failure(RecordFailure( - log, &record, residual.GetStatus(), - NewtonIterationStage::kExternalAssembled)); - } - Result tangent = - Difference(internal.Value().tangent, - external.Value().generalized_external_tangent, - dofs.Value()); - if (!tangent.HasValue()) { - return Result::Failure(RecordFailure( - log, &record, tangent.GetStatus(), - NewtonIterationStage::kExternalAssembled)); - } - trial.Value().internal_force = std::move(internal.Value().force); - trial.Value().external_force = - std::move(external.Value().generalized_external_force); - trial.Value().residual = std::move(residual.Value()); - trial.Value().reaction = trial.Value().residual; - trial.Value().shell_results = std::move(internal.Value().shell_rows); - trial.Value().physical_strain_energy = - internal.Value().physical_strain_energy; - Result> global_equilibrium = - GlobalEquilibrium(domain, dofs.Value(), trial.Value()); - if (!global_equilibrium.HasValue()) { - return Result::Failure(RecordFailure( - log, &record, global_equilibrium.GetStatus(), - NewtonIterationStage::kExternalAssembled)); - } - trial.Value().global_equilibrium = global_equilibrium.Value(); + Result residual = + Difference(internal.Value().force, + external.Value().generalized_external_force); + if (!residual.HasValue()) { + return Result>::Failure( + report_failure(&record, residual.GetStatus(), + NewtonIterationStage::kExternalAssembled)); + } + Result tangent = Difference( + internal.Value().tangent, + external.Value().generalized_external_tangent, dofs.Value()); + if (!tangent.HasValue()) { + return Result>::Failure( + report_failure(&record, tangent.GetStatus(), + NewtonIterationStage::kExternalAssembled)); + } + trial.Value().internal_force = std::move(internal.Value().force); + trial.Value().external_force = + std::move(external.Value().generalized_external_force); + trial.Value().residual = std::move(residual.Value()); + trial.Value().reaction = trial.Value().residual; + trial.Value().shell_results = std::move(internal.Value().shell_rows); + trial.Value().physical_strain_energy = + internal.Value().physical_strain_energy; + Result> global_equilibrium = + GlobalEquilibrium(domain, dofs.Value(), trial.Value()); + if (!global_equilibrium.HasValue()) { + return Result>::Failure( + report_failure(&record, global_equilibrium.GetStatus(), + NewtonIterationStage::kExternalAssembled)); + } + trial.Value().global_equilibrium = global_equilibrium.Value(); - Result partition = - constraints.Partition(tangent.Value(), dofs.Value()); - if (!partition.HasValue()) { - return Result::Failure(RecordFailure( - log, &record, partition.GetStatus(), - NewtonIterationStage::kExternalAssembled)); - } - const Status factorization = tangent_solver_.FactorizeFresh( - partition.Value().k_ff); - if (!factorization.IsOk()) { - return Result::Failure(RecordFailure( - log, &record, factorization, - NewtonIterationStage::kSolvedCorrection)); - } + Result partition = + constraints.Partition(tangent.Value(), dofs.Value()); + if (!partition.HasValue()) { + return Result>::Failure( + report_failure(&record, partition.GetStatus(), + NewtonIterationStage::kExternalAssembled)); + } + const Status factorization = + tangent_solver.FactorizeFresh(partition.Value().k_ff); + if (!factorization.IsOk()) { + return Result>::Failure( + report_failure(&record, factorization, + NewtonIterationStage::kSolvedCorrection)); + } - Vector correction_rhs = constraints.GatherFree(trial.Value().residual, - dofs.Value()); - for (std::size_t index = 0U; index < correction_rhs.Size(); ++index) { - correction_rhs[index] = -correction_rhs[index]; - } - Vector correction{dofs.Value().FreeDofCount()}; - const Status solve = - tangent_solver_.SolveCorrection(correction_rhs, correction); - if (!solve.IsOk()) { - return Result::Failure(RecordFailure( - log, &record, solve, NewtonIterationStage::kSolvedCorrection)); - } - record.last_completed_stage = NewtonIterationStage::kSolvedCorrection; + Vector correction_rhs = + constraints.GatherFree(trial.Value().residual, dofs.Value()); + for (std::size_t index = 0U; index < correction_rhs.Size(); ++index) { + correction_rhs[index] = -correction_rhs[index]; + } + Vector correction{dofs.Value().FreeDofCount()}; + const Status solve = + tangent_solver.SolveCorrection(correction_rhs, correction); + if (!solve.IsOk()) { + return Result>::Failure( + report_failure(&record, solve, + NewtonIterationStage::kSolvedCorrection)); + } + record.last_completed_stage = NewtonIterationStage::kSolvedCorrection; - const NewtonConvergenceResult convergence = EvaluateNewtonConvergence({ - {"residual_force", SelectFreeComponents(trial.Value().residual, - dofs.Value(), false), - SelectFreeComponents(trial.Value().internal_force, dofs.Value(), - false), - SelectFreeComponents(trial.Value().external_force, dofs.Value(), - false)}, - {"residual_moment", SelectFreeComponents(trial.Value().residual, - dofs.Value(), true), - SelectFreeComponents(trial.Value().internal_force, dofs.Value(), - true), - SelectFreeComponents(trial.Value().external_force, dofs.Value(), - true)}, - {"correction_translation", - SelectCorrectionComponents(correction, dofs.Value(), false), - CurrentIncrementComponents(trial.Value().generalized_coordinates, - increment_start, dofs.Value(), false)}, - {"correction_rotation", - SelectCorrectionComponents(correction, dofs.Value(), true), - CurrentIncrementComponents(trial.Value().generalized_coordinates, - increment_start, dofs.Value(), true)}}); - record.force_residual = convergence.force_residual; - record.moment_residual = convergence.moment_residual; - record.translation_correction = convergence.translation_correction; - record.rotation_correction = convergence.rotation_correction; - record.combined_passed = convergence.combined_passed; - record.last_completed_stage = NewtonIterationStage::kConvergenceEvaluated; + const NewtonConvergenceResult convergence = EvaluateNewtonConvergence( + {{"residual_force", + SelectFreeComponents(trial.Value().residual, dofs.Value(), + false), + SelectFreeComponents(trial.Value().internal_force, dofs.Value(), + false), + SelectFreeComponents(trial.Value().external_force, dofs.Value(), + false)}, + {"residual_moment", + SelectFreeComponents(trial.Value().residual, dofs.Value(), + true), + SelectFreeComponents(trial.Value().internal_force, dofs.Value(), + true), + SelectFreeComponents(trial.Value().external_force, dofs.Value(), + true)}, + {"correction_translation", + SelectCorrectionComponents(correction, dofs.Value(), false), + CurrentIncrementComponents( + trial.Value().generalized_coordinates, increment_start, + dofs.Value(), false)}, + {"correction_rotation", + SelectCorrectionComponents(correction, dofs.Value(), true), + CurrentIncrementComponents( + trial.Value().generalized_coordinates, increment_start, + dofs.Value(), true)}}); + record.force_residual = convergence.force_residual; + record.moment_residual = convergence.moment_residual; + record.translation_correction = convergence.translation_correction; + record.rotation_correction = convergence.rotation_correction; + record.combined_passed = convergence.combined_passed; + record.last_completed_stage = + NewtonIterationStage::kConvergenceEvaluated; - if (convergence.combined_passed) { - const Status committed = state.Value().CommitAcceptedFrame( - AcceptedFrame(state.Value(), trial.Value(), increment, iteration)); - if (!committed.IsOk()) { - return Result::Failure(RecordFailure( - log, &record, committed, NewtonIterationStage::kAccepted)); - } - record.last_completed_stage = NewtonIterationStage::kAccepted; - record.transition = "accepted"; - record.outcome = "accepted"; - const Status log_status = WriteAndFlush(log, record); - if (!log_status.IsOk()) { - return Result::Failure(log_status); - } - break; - } + if (convergence.combined_passed) { + return Result>::Success( + ShellStepCandidate{AcceptedFrame(next_frame_index, + trial.Value(), increment, + iteration), + record}); + } - if (iteration == kMaximumNewtonIterations) { - const Status failure = ProcedureFailure( - FailureCategory::kSolver, "nonlinear-nonconvergence", - step.Location(), "STATIC", step.Name(), - "The nonlinear procedure did not converge within 100 iterations."); - return Result::Failure(RecordFailure( - log, &record, failure, - NewtonIterationStage::kConvergenceEvaluated)); - } + if (iteration == kMaximumNewtonIterations) { + const Status failure = ProcedureFailure( + FailureCategory::kSolver, "nonlinear-nonconvergence", + step.Location(), "STATIC", step.Name(), + "The nonlinear procedure did not converge within 100 " + "iterations."); + return Result>::Failure( + report_failure(&record, failure, + NewtonIterationStage::kConvergenceEvaluated)); + } - for (std::size_t equation = 0U; equation < dofs.Value().FreeDofs().size(); - ++equation) { - const std::size_t full_dof = dofs.Value().FreeDofs()[equation]; - trial.Value().generalized_coordinates[full_dof] += correction[equation]; - if (!std::isfinite(trial.Value().generalized_coordinates[full_dof])) { - const Status failure = ProcedureFailure( - FailureCategory::kSolver, "nonfinite-newton-data", {}, - "NONLINEAR_STATIC_PROCEDURE", std::to_string(full_dof), - "A nonlinear free-coordinate correction is nonfinite."); - return Result::Failure(RecordFailure( - log, &record, failure, NewtonIterationStage::kSolvedCorrection)); - } - } - record.transition = "continue"; - record.outcome = "not-converged"; - const Status log_status = WriteAndFlush(log, record); - if (!log_status.IsOk()) { - return Result::Failure(log_status); - } + for (std::size_t equation = 0U; + equation < dofs.Value().FreeDofs().size(); ++equation) { + const std::size_t full_dof = dofs.Value().FreeDofs()[equation]; + trial.Value().generalized_coordinates[full_dof] += + correction[equation]; + if (!std::isfinite( + trial.Value().generalized_coordinates[full_dof])) { + const Status failure = ProcedureFailure( + FailureCategory::kSolver, "nonfinite-newton-data", {}, + "NONLINEAR_STATIC_PROCEDURE", std::to_string(full_dof), + "A nonlinear free-coordinate correction is nonfinite."); + return Result>::Failure( + report_failure(&record, failure, + NewtonIterationStage::kSolvedCorrection)); + } + } + record.transition = "continue"; + record.outcome = "not-converged"; + const Status log_status = write_iteration(record); + if (!log_status.IsOk()) { + return Result>::Failure( + log_status); + } + return Result>::Success( + std::nullopt); + }, + kMaximumNewtonIterations); + const Status solved = iteration_step.Solve(algorithm); + if (!solved.IsOk()) + return Result::Failure(solved); + auto candidate = iteration_step.TakeCandidate(); + auto& record = candidate.record; + const Status committed = state.Value().CommitAcceptedFrame(candidate.frame); + if (!committed.IsOk()) { + return Result::Failure(RecordFailure( + log, &record, committed, NewtonIterationStage::kAccepted)); } + record.last_completed_stage = NewtonIterationStage::kAccepted; + record.transition = "accepted"; + record.outcome = "accepted"; + const Status log_status = WriteAndFlush(log, record); + if (!log_status.IsOk()) + return Result::Failure(log_status); } std::vector diagnostics = domain.Warnings(); @@ -983,4 +1013,4 @@ NonlinearStaticProcedureExecutor::Execute( {std::move(state.Value()), std::move(diagnostics)}); } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/analysis/prepared_analysis.cpp b/src/fesa/analysis/prepared_analysis.cpp index fff5a2e..f562a4e 100644 --- a/src/fesa/analysis/prepared_analysis.cpp +++ b/src/fesa/analysis/prepared_analysis.cpp @@ -1,7 +1,10 @@ #include "fesa/analysis/prepared_analysis.h" +#include #include +#include "analysis_procedures.h" + #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/hht_dynamic_procedure.h" #include "fesa/analysis/linear_frequency_procedure.h" @@ -9,15 +12,13 @@ #include "fesa/analysis/newton_iteration_log.h" #include "fesa/analysis/nonlinear_static_procedure.h" #include "fesa/analysis/solid_frequency_procedure.h" +#include "fesa/analysis/solid_hht_dynamic_procedure.h" +#include "fesa/analysis/solid_static_procedure.h" +#include "fesa/analysis/transient_heat_procedure.h" #include "fesa/app/output_paths.h" #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" -#include "fesa/results/dynamic_results_writer.h" -#include "fesa/results/eigen_results_writer.h" -#include "fesa/results/nonlinear_results_writer.h" -#include "fesa/results/results_writer.h" -#include "fesa/results/solid_results_writer.h" -#include "fesa/results/thermal_results_writer.h" +#include "fesa/results/prepared_results_writer.h" namespace fesa { @@ -39,6 +40,185 @@ Result PrepareAnalysisInput( return Result::Success(std::move(prepared)); } +namespace { + +enum class ProcedureKind { + kLinearStatic, + kNonlinearStatic, + kLinearFrequency, + kHhtDynamic, + kSolidStatic, + kSolidFrequency, + kSolidHhtDynamic, + kTransientHeat +}; + +// Keep procedure precedence and step-count diagnostics common to both APIs. +Result SelectProcedure(const Domain& domain) { + if (domain.ThermalStep() != nullptr) + return Result::Success(ProcedureKind::kTransientHeat); + if (domain.Steps().Size() != 1U) { + return Result::Failure(Status::Failure( + FailureCategory::kInput, + {{Severity::kError, + "invalid-analysis-step-count", + {}, + "*STEP", + {}, + "Prepared analysis requires exactly one analysis step."}})); + } + if (domain.IsSolid()) { + if (domain.Dynamic() != nullptr) + return Result::Success(ProcedureKind::kSolidHhtDynamic); + if (domain.Steps()[0U].IsFrequency()) + return Result::Success(ProcedureKind::kSolidFrequency); + return Result::Success(ProcedureKind::kSolidStatic); + } + if (domain.Dynamic() != nullptr) + return Result::Success(ProcedureKind::kHhtDynamic); + if (domain.Steps()[0U].IsFrequency()) + return Result::Success(ProcedureKind::kLinearFrequency); + if (domain.Steps()[0U].IsNonlinearStatic()) + return Result::Success(ProcedureKind::kNonlinearStatic); + return Result::Success(ProcedureKind::kLinearStatic); +} + +template +Result Completed(Result result) { + if (!result.HasValue()) + return Result::Failure(result.GetStatus()); + return Result::Success( + PreparedAnalysisOutcome{std::move(result.Value())}); +} + +Result +MissingExecutor(const char* code, const char* keyword, const char* message) { + return Result::Failure( + Status::Failure(FailureCategory::kSolver, + {{Severity::kError, code, {}, keyword, {}, message}})); +} + +} // namespace + +Result +LinearStaticProcedure::Execute(const Domain& domain) { + return Completed(executor_.Execute(domain)); +} + +Result +NonlinearStaticProcedure::Execute(const Domain& domain) { + return Completed(executor_.Execute(domain, log_)); +} + +Result +LinearFrequencyProcedure::Execute(const Domain& domain) { + return Completed(executor_.Execute(domain)); +} + +Result +HhtDynamicProcedure::Execute(const Domain& domain) { + return Completed(executor_.Execute(domain, log_)); +} + +Result +SolidStaticProcedure::Execute(const Domain& domain) { + SolidStaticState state; + const Status execution = executor_.ExecuteInto(domain, state); + for (const auto& record : state.IterationRecords()) { + const Status written = log_.Write(record, domain.Steps()[0U].Name()); + if (!written.IsOk()) + return Result::Failure(written); + } + if (!execution.IsOk()) + return Result::Failure(execution); + return Result::Success(PreparedAnalysisOutcome{ + SolidStaticExecutionResult{std::move(state), domain.Warnings()}}); +} + +Result +SolidFrequencyProcedure::Execute(const Domain& domain) { + return Completed(executor_.Execute(domain)); +} + +Result +SolidHhtDynamicProcedure::Execute(const Domain& domain) { + auto model = AnalysisModel::Create(domain); + if (!model.HasValue()) + return Result::Failure(model.GetStatus()); + auto dofs = DofManager::Create(model.Value()); + if (!dofs.HasValue()) + return Result::Failure(dofs.GetStatus()); + auto state = SolidDynamicAnalysisState::Create(domain, dofs.Value()); + if (!state.HasValue()) + return Result::Failure(state.GetStatus()); + const Status execution = executor_.ExecuteInto(domain, state.Value()); + for (const auto& record : state.Value().IterationRecords()) { + const Status written = log_.Write(record); + if (!written.IsOk()) + return Result::Failure(written); + } + if (!execution.IsOk()) + return Result::Failure(execution); + return Result::Success( + PreparedAnalysisOutcome{SolidDynamicExecutionResult{ + std::move(dofs.Value()), std::move(state.Value())}}); +} + +Result +TransientHeatProcedure::Execute(const Domain& domain) { + return Completed(executor_.Execute(domain)); +} + +Result> CreateAnalysisProcedure( + const Domain& domain, const ParallelFor& parallel_for, + LinearSolver& linear_solver, TangentSolver& tangent_solver, + const EigenSolver& eigen_solver, NewtonIterationLogWriter& log) { + const auto kind = SelectProcedure(domain); + if (!kind.HasValue()) + return Result>::Failure( + kind.GetStatus()); + std::unique_ptr procedure; + switch (kind.Value()) { + case ProcedureKind::kLinearStatic: + procedure = + std::make_unique(parallel_for, linear_solver); + break; + case ProcedureKind::kNonlinearStatic: + procedure = std::make_unique(parallel_for, + tangent_solver, log); + break; + case ProcedureKind::kLinearFrequency: + procedure = std::make_unique(eigen_solver); + break; + case ProcedureKind::kHhtDynamic: + procedure = std::make_unique( + parallel_for, linear_solver, tangent_solver, log); + break; + case ProcedureKind::kSolidStatic: + procedure = std::make_unique( + parallel_for, linear_solver, tangent_solver, log); + break; + case ProcedureKind::kSolidFrequency: + procedure = std::make_unique(eigen_solver); + break; + case ProcedureKind::kSolidHhtDynamic: + procedure = std::make_unique( + parallel_for, linear_solver, tangent_solver, log); + break; + case ProcedureKind::kTransientHeat: + procedure = + std::make_unique(parallel_for, linear_solver); + break; + } + return Result>::Success( + std::move(procedure)); +} + +Result +ExecutePreparedAnalysis(const Domain& domain, AnalysisProcedure& procedure) { + return procedure.Execute(domain); +} + Result ExecutePreparedAnalysis( const Domain& domain, LinearStaticProcedureExecutor& linear_executor, NonlinearStaticProcedureExecutor& nonlinear_executor, @@ -49,133 +229,59 @@ Result ExecutePreparedAnalysis( const SolidStaticProcedureExecutor* solid_static_executor, const SolidFrequencyProcedureExecutor* solid_frequency_executor, const SolidHhtDynamicProcedureExecutor* solid_dynamic_executor) { - if (domain.ThermalStep() != nullptr) { - if (thermal_executor == nullptr) { - return Result::Failure( - Status::Failure(FailureCategory::kSolver, - {{Severity::kError, - "missing-thermal-executor", - {}, - "HEAT TRANSFER", - {}, - "Thermal execution adapter is required."}})); - } - auto thermal = thermal_executor->Execute(domain); - if (!thermal.HasValue()) { - return Result::Failure(thermal.GetStatus()); - } - return Result::Success( - PreparedAnalysisOutcome{std::move(thermal.Value())}); - } - if (domain.Steps().Size() != 1U) { - return Result::Failure(Status::Failure( - FailureCategory::kInput, - {{Severity::kError, - "invalid-analysis-step-count", - {}, - "*STEP", - {}, - "Prepared analysis requires exactly one analysis step."}})); - } - - if (domain.IsSolid()) { - const auto missing = []() { - return Result::Failure(Status::Failure( - FailureCategory::kSolver, - {{Severity::kError, - "missing-solid-executor", - {}, - "", - {}, - "The selected Solid execution adapter is required."}})); - }; - if (domain.Dynamic() != nullptr) { - if (solid_dynamic_executor == nullptr) return missing(); - auto model = AnalysisModel::Create(domain); - if (!model.HasValue()) - return Result::Failure(model.GetStatus()); - auto dofs = DofManager::Create(model.Value()); - if (!dofs.HasValue()) - return Result::Failure(dofs.GetStatus()); - auto state = SolidDynamicAnalysisState::Create(domain, dofs.Value()); - if (!state.HasValue()) - return Result::Failure(state.GetStatus()); - const Status execution = - solid_dynamic_executor->ExecuteInto(domain, state.Value()); - for (const auto& record : state.Value().IterationRecords()) { - const Status written = log.Write(record); - if (!written.IsOk()) - return Result::Failure(written); - } - if (!execution.IsOk()) - return Result::Failure(execution); - return Result::Success( - PreparedAnalysisOutcome{SolidDynamicExecutionResult{ - std::move(dofs.Value()), std::move(state.Value())}}); - } - if (domain.Steps()[0U].IsFrequency()) { - if (solid_frequency_executor == nullptr) return missing(); - auto result = solid_frequency_executor->Execute(domain); - if (!result.HasValue()) - return Result::Failure(result.GetStatus()); - return Result::Success( - PreparedAnalysisOutcome{std::move(result.Value())}); - } - if (solid_static_executor == nullptr) return missing(); - SolidStaticState state; - const Status execution = solid_static_executor->ExecuteInto(domain, state); - for (const auto& record : state.IterationRecords()) { - const Status written = log.Write(record, domain.Steps()[0U].Name()); - if (!written.IsOk()) - return Result::Failure(written); - } - if (!execution.IsOk()) - return Result::Failure(execution); - return Result::Success(PreparedAnalysisOutcome{ - SolidStaticExecutionResult{std::move(state), domain.Warnings()}}); - } - - if (domain.Dynamic() != nullptr) { + const auto kind = SelectProcedure(domain); + if (!kind.HasValue()) + return Result::Failure(kind.GetStatus()); + std::unique_ptr procedure; + switch (kind.Value()) { + case ProcedureKind::kLinearStatic: + procedure = std::make_unique(linear_executor); + break; + case ProcedureKind::kNonlinearStatic: + procedure = + std::make_unique(nonlinear_executor, log); + break; + case ProcedureKind::kLinearFrequency: + procedure = std::make_unique(frequency_executor); + break; + case ProcedureKind::kHhtDynamic: if (dynamic_executor == nullptr) - return Result::Failure( - Status::Failure(FailureCategory::kSolver, - {{Severity::kError, - "missing-dynamic-executor", - {}, - "DYNAMIC", - {}, - "Dynamic execution adapter is required."}})); - auto dynamic = dynamic_executor->Execute(domain, log); - if (!dynamic.HasValue()) - return Result::Failure(dynamic.GetStatus()); - return Result::Success( - PreparedAnalysisOutcome{std::move(dynamic.Value())}); + return MissingExecutor("missing-dynamic-executor", "DYNAMIC", + "Dynamic execution adapter is required."); + procedure = std::make_unique(*dynamic_executor, log); + break; + case ProcedureKind::kSolidStatic: + if (solid_static_executor == nullptr) + return MissingExecutor( + "missing-solid-executor", "", + "The selected Solid execution adapter is required."); + procedure = + std::make_unique(*solid_static_executor, log); + break; + case ProcedureKind::kSolidFrequency: + if (solid_frequency_executor == nullptr) + return MissingExecutor( + "missing-solid-executor", "", + "The selected Solid execution adapter is required."); + procedure = + std::make_unique(*solid_frequency_executor); + break; + case ProcedureKind::kSolidHhtDynamic: + if (solid_dynamic_executor == nullptr) + return MissingExecutor( + "missing-solid-executor", "", + "The selected Solid execution adapter is required."); + procedure = std::make_unique( + *solid_dynamic_executor, log); + break; + case ProcedureKind::kTransientHeat: + if (thermal_executor == nullptr) + return MissingExecutor("missing-thermal-executor", "HEAT TRANSFER", + "Thermal execution adapter is required."); + procedure = std::make_unique(*thermal_executor); + break; } - if (domain.Steps()[0U].IsFrequency()) { - auto eigen = frequency_executor.Execute(domain); - if (!eigen.HasValue()) { - return Result::Failure(eigen.GetStatus()); - } - return Result::Success( - PreparedAnalysisOutcome{std::move(eigen.Value())}); - } - - if (domain.Steps()[0U].IsNonlinearStatic()) { - Result nonlinear = - nonlinear_executor.Execute(domain, log); - if (!nonlinear.HasValue()) { - return Result::Failure(nonlinear.GetStatus()); - } - return Result::Success( - PreparedAnalysisOutcome{std::move(nonlinear.Value())}); - } - - Result linear = linear_executor.Execute(domain); - if (!linear.HasValue()) { - return Result::Failure(linear.GetStatus()); - } - return Result::Success( - PreparedAnalysisOutcome{std::move(linear.Value())}); + return ExecutePreparedAnalysis(domain, *procedure); } Status FinalizePreparedAnalysisResult( @@ -185,68 +291,10 @@ Status FinalizePreparedAnalysisResult( NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer, DynamicResultsWriter* dynamic_writer, ThermalResultsWriter* thermal_writer, SolidResultsWriter* solid_writer) { - if (std::holds_alternative(outcome) || - std::holds_alternative(outcome) || - std::holds_alternative(outcome)) { - if (solid_writer == nullptr) - return Status::Failure(FailureCategory::kOutput, - {{Severity::kError, - "missing-solid-writer", - {}, - "", - {}, - "Solid result writer is required."}}); - if (const auto* result = std::get_if(&outcome)) - return solid_writer->Write(paths.results_path, domain, result->state, - diagnostics); - if (const auto* result = std::get_if(&outcome)) - return solid_writer->Write(paths.results_path, domain, *result, - diagnostics); - return solid_writer->Write( - paths.results_path, domain, - std::get(outcome).state, diagnostics); - } - if (std::holds_alternative(outcome)) { - if (thermal_writer == nullptr) { - return Status::Failure(FailureCategory::kOutput, - {{Severity::kError, - "missing-thermal-writer", - {}, - "HEAT TRANSFER", - {}, - "Thermal result writer is required."}}); - } - return thermal_writer->Write( - paths.results_path, domain, - std::get(outcome).state, diagnostics); - } - if (std::holds_alternative(outcome)) { - if (dynamic_writer == nullptr) - return Status::Failure(FailureCategory::kOutput, - {{Severity::kError, - "missing-dynamic-writer", - {}, - "DYNAMIC", - {}, - "Dynamic result writer is required."}}); - return dynamic_writer->Write( - paths.results_path, domain, - std::get(outcome).state, diagnostics); - } - if (std::holds_alternative(outcome)) { - return eigen_writer.Write(paths.results_path, domain, - std::get(outcome), - diagnostics); - } - if (std::holds_alternative(outcome)) { - return linear_writer.Write( - paths.results_path, domain, - std::get(outcome).state, diagnostics); - } - - return nonlinear_writer.Write( - paths.results_path, domain, - std::get(outcome).state, diagnostics); + return WritePreparedAnalysisResult( + paths.results_path, domain, outcome, diagnostics, linear_writer, + nonlinear_writer, eigen_writer, dynamic_writer, thermal_writer, + solid_writer); } } // namespace fesa diff --git a/src/fesa/analysis/shell_state_inventory.h b/src/fesa/analysis/shell_state_inventory.h new file mode 100644 index 0000000..f18fd5a --- /dev/null +++ b/src/fesa/analysis/shell_state_inventory.h @@ -0,0 +1,25 @@ +#ifndef FESA_ANALYSIS_SHELL_STATE_INVENTORY_H_ +#define FESA_ANALYSIS_SHELL_STATE_INVENTORY_H_ + +#include +#include + +#include "fesa/elements/element_definition.h" +#include "fesa/model/domain.h" + +namespace fesa::analysis_internal { + +/// @brief Collects unified MITC4 element indices in stable Domain order. +inline std::vector ShellElementOrder(const Domain& domain) { + std::vector order; + for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { + if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) { + order.push_back(static_cast(index)); + } + } + return order; +} + +} // namespace fesa::analysis_internal + +#endif // FESA_ANALYSIS_SHELL_STATE_INVENTORY_H_ diff --git a/src/fesa/analysis/solid_dynamic_analysis_state.cpp b/src/fesa/analysis/solid_dynamic_analysis_state.cpp index d5e2cab..c9bf86d 100644 --- a/src/fesa/analysis/solid_dynamic_analysis_state.cpp +++ b/src/fesa/analysis/solid_dynamic_analysis_state.cpp @@ -1,13 +1,15 @@ #include "fesa/analysis/solid_dynamic_analysis_state.h" -#include #include #include #include +#include "fesa/core/source_identity.h" #include "fesa/elements/element_factory.h" #include "fesa/elements/hex8_element.h" +#include "fesa/model/domain.h" #include "math/high_low_arithmetic.h" +#include "math/finite_values.h" namespace fesa { namespace { @@ -24,8 +26,7 @@ Status Failure(const std::string& message) { /// @brief Checks a physical record without replacing nonfinite quantities. template bool Finite(const std::array& values) { - return std::all_of(values.begin(), values.end(), - [](double x) { return std::isfinite(x); }); + return math_internal::IsFinite(values); } bool ValidPair(double high, double low) { const math_internal::HighLow value(high, low); @@ -44,12 +45,6 @@ bool ValidPair(const std::array& high, 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::Create( @@ -83,7 +78,7 @@ Result SolidDynamicAnalysisState::Create( auto runtime = factory.Create(domain.Elements()[e], domain); if (!runtime.HasValue()) return Result::Failure(runtime.GetStatus()); - const auto* solid = dynamic_cast(runtime.Value().get()); + auto solid = factory.BindSolidTrial(*runtime.Value()); if (solid == nullptr) return Result::Failure(Failure( "Dynamic Solid inventory contains a different element family.")); @@ -117,7 +112,7 @@ Status SolidDynamicAnalysisState::CommitAcceptedFrame( } 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])) + !SourceIdentityEquals(f.nodal_source_ids[n], source_ids_[n])) return Failure("Solid nodal source identity differs from its inventory."); const std::array high{ &f.displacement, &f.velocity, diff --git a/src/fesa/analysis/solid_frequency_procedure.cpp b/src/fesa/analysis/solid_frequency_procedure.cpp index f3a33dc..b035ce2 100644 --- a/src/fesa/analysis/solid_frequency_procedure.cpp +++ b/src/fesa/analysis/solid_frequency_procedure.cpp @@ -18,6 +18,7 @@ #include "fesa/elements/hex8_element.h" #include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" +#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h" #include "fesa/math/vector.h" #include "fesa/model/domain.h" @@ -50,7 +51,8 @@ double Ratio(double numerator, double denominator) { return numerator / denominator; } /// @brief Builds an immutable candidate; all returns precede public commit. -Result ExecuteCandidate(const Domain& domain) { +Result ExecuteCandidate(const Domain& domain, + const EigenSolver& solver) { const auto* frequency = domain.Frequency(); if (!domain.IsSolid() || frequency == nullptr || frequency->requested_mode_count <= 0) @@ -76,6 +78,7 @@ Result ExecuteCandidate(const Domain& domain) { } ElementFactory factory; std::vector> owned; + std::vector> trials; ElementView view; for (const auto index : model.Value().ActiveElements()) { const auto& definition = domain.Elements()[index]; @@ -87,11 +90,13 @@ Result ExecuteCandidate(const Domain& domain) { auto element = factory.Create(definition, domain); if (!element.HasValue()) return Result::Failure(element.GetStatus()); - if (dynamic_cast(element.Value().get()) == nullptr) + auto trial = factory.BindSolidTrial(*element.Value()); + if (trial == nullptr) return Result::Failure( Failure("unsupported-frequency-element", "Only C3D8 is supported.")); view.emplace_back(*element.Value()); owned.push_back(std::move(element.Value())); + trials.push_back(std::move(trial)); } DofManager dofs; const Status mapping = dofs.Build(model.Value(), view); @@ -121,7 +126,7 @@ Result ExecuteCandidate(const Domain& domain) { std::vector additions_by_dof(dofs.FullDofCount(), 0U); constexpr double eps = std::numeric_limits::epsilon(); for (std::size_t e = 0; e < owned.size(); ++e) { - const auto& hex = static_cast(*owned[e]); + const auto& hex = *trials[e]; auto mass = hex.ComputeMass(); if (!mass.HasValue()) return Result::Failure(mass.GetStatus()); @@ -220,7 +225,7 @@ Result ExecuteCandidate(const Domain& domain) { return Result::Failure( Failure("invalid-solid-eigen-scale", "A positive finite classification scale is required.")); - auto backend = MklFeastEigenSolver{}.SolveGeneralized(k, m, requested, + auto backend = solver.SolveGeneralized(k, m, requested, mass_lower, scale); if (!backend.HasValue()) return Result::Failure(backend.GetStatus()); @@ -269,11 +274,11 @@ Result ExecuteCandidate(const Domain& domain) { "Normalized modal vector is not finite.")); } mv = m.Multiply(vector); - const Vector kv = k.Multiply(vector); + Vector kv = k.Multiply(vector); const double lambda = state.backend.eigenvalues[c]; const double modal_mass = vector.Dot(mv); const double energy = vector.Dot(kv); - Vector residual = kv; + Vector residual = std::move(kv); residual.Axpy(-lambda, mv); const double norm_phi = vector.Norm(); const double residual_norm = residual.Norm(); @@ -355,7 +360,8 @@ Result ExecuteCandidate(const Domain& domain) { Result SolidFrequencyProcedureExecutor::Execute( const Domain& domain) const { try { - auto result = ExecuteCandidate(domain); + const MklFeastEigenSolver default_solver; + auto result = ExecuteCandidate(domain, solver_ ? *solver_ : default_solver); if (result.HasValue()) return result; auto diagnostics = result.GetStatus().Diagnostics(); for (auto& diagnostic : diagnostics) diff --git a/src/fesa/analysis/solid_hht_core.h b/src/fesa/analysis/solid_hht_core.h new file mode 100644 index 0000000..75f93a1 --- /dev/null +++ b/src/fesa/analysis/solid_hht_core.h @@ -0,0 +1,20 @@ +#ifndef FESA_ANALYSIS_SOLID_HHT_CORE_H_ +#define FESA_ANALYSIS_SOLID_HHT_CORE_H_ +#include "fesa/core/status.h" +namespace fesa { +namespace detail { class SolidHhtIntegrator; } +class Domain; +class SolidDynamicAnalysisState; +class ParallelFor; +class LinearSolver; +class TangentSolver; +class EquilibriumAlgorithm; +/// @brief Runs the complete production core with borrowed iteration policy. +/// @note Adapters outlive this synchronous call; history remains state-owned. +Status ExecuteSolidHhtCore( + const Domain& domain, SolidDynamicAnalysisState& state, double alpha, + const ParallelFor& parallel_for, LinearSolver& mass_solver, + TangentSolver& tangent_solver, const detail::SolidHhtIntegrator& integrator, + EquilibriumAlgorithm& algorithm); +} // namespace fesa +#endif // FESA_ANALYSIS_SOLID_HHT_CORE_H_ diff --git a/src/fesa/analysis/solid_hht_dynamic_procedure.cpp b/src/fesa/analysis/solid_hht_dynamic_procedure.cpp index f0a833f..d5f5d89 100644 --- a/src/fesa/analysis/solid_hht_dynamic_procedure.cpp +++ b/src/fesa/analysis/solid_hht_dynamic_procedure.cpp @@ -1,5 +1,9 @@ #include "fesa/analysis/solid_hht_dynamic_procedure.h" +#include "equilibrium_algorithm.h" +#include "analysis/solid_hht_core.h" +#include "solid_hht_integrator.h" + #include #include #include @@ -85,7 +89,7 @@ Status CheckArithmeticEnvironment(const Domain& domain) { /// @brief Owns immutable kernels and the stable full-space translation scatter. struct RuntimeSolid { std::unique_ptr owner; - const Hex8Element* solid; + std::unique_ptr solid; std::vector scatter; }; /// @brief Creates checked kernels in declaration order, with no mutable element @@ -98,7 +102,7 @@ Result> BuildSolids(const Domain& domain, auto owner = factory.Create(domain.Elements()[e], domain); if (!owner.HasValue()) return Result>::Failure(owner.GetStatus()); - const auto* solid = dynamic_cast(owner.Value().get()); + auto solid = factory.BindSolidTrial(*owner.Value()); if (solid == nullptr) return Result>::Failure( Failure(domain, "solid-dynamic-element-family", @@ -107,7 +111,7 @@ Result> BuildSolids(const Domain& domain, if (!scatter.HasValue()) return Result>::Failure(scatter.GetStatus()); solids.push_back( - {std::move(owner.Value()), solid, std::move(scatter.Value())}); + {std::move(owner.Value()), std::move(solid), std::move(scatter.Value())}); } return Result>::Success(std::move(solids)); } @@ -239,7 +243,7 @@ Result ReferenceLength(const Domain& domain) { "Reference length is not finite positive.")); return Result::Success(length); } -/// @brief Forms M/(beta*h²)+(1+alpha)*Kt in the unchanged full sparse pattern. +/// @brief Forms M/(beta*h^2)+(1+alpha)*Kt in the unchanged full sparse pattern. Result EffectiveTangent(const SparseMatrix& m, const PairedTangent& k, const DofManager& dofs, double h, @@ -549,24 +553,6 @@ bool CorrectionPass(const std::array& metrics) { 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( @@ -600,6 +586,17 @@ Status SolidHhtDynamicProcedureExecutor::ExecuteInto( Status SolidHhtDynamicProcedureExecutor::ExecuteIntoForVerification( const Domain& domain, SolidDynamicAnalysisState& state, double alpha) const { + const detail::PairedSolidHhtIntegrator integrator; + NewtonStepAlgorithm algorithm; + return ExecuteSolidHhtCore(domain, state, alpha, parallel_for_, mass_solver_, + tangent_solver_, integrator, algorithm); +} + +Status ExecuteSolidHhtCore( + const Domain& domain, SolidDynamicAnalysisState& state, double alpha, + const ParallelFor& parallel_for, LinearSolver& mass_solver, + TangentSolver& tangent_solver, const detail::SolidHhtIntegrator& integrator, + EquilibriumAlgorithm& algorithm) { SolidDynamicIterationRecord record; auto fail = [&](const Status& failure) { record.stage = "failed"; @@ -630,7 +627,7 @@ Status SolidHhtDynamicProcedureExecutor::ExecuteIntoForVerification( 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); + 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); @@ -655,7 +652,7 @@ Status SolidHhtDynamicProcedureExecutor::ExecuteIntoForVerification( if (!force.HasValue()) return fail(force.GetStatus()); initial.external_force = std::move(force.Value()); auto tangent = - EvaluateSolids(domain, solids.Value(), dofs, parallel_for_, &initial); + EvaluateSolids(domain, solids.Value(), dofs, parallel_for, &initial); if (!tangent.HasValue()) return fail(tangent.GetStatus()); SolidDynamicIterationEvidence evidence; bool initial_converged = false; @@ -682,7 +679,7 @@ Status SolidHhtDynamicProcedureExecutor::ExecuteIntoForVerification( const auto i = dofs.FreeDofs()[j]; rhs[j] = Rounded(Negate(Pair(initial.residual, initial.low.residual, i))); } - status = mass_solver_.Solve(rhs, a); + status = mass_solver.Solve(rhs, a); if (!status.IsOk()) return fail(status); environment = CheckArithmeticEnvironment(domain); if (!environment.IsOk()) return fail(environment); @@ -726,145 +723,188 @@ Status SolidHhtDynamicProcedureExecutor::ExecuteIntoForVerification( record = {}; record.increment_index = increment; record.time = static_cast(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 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.")); + status = detail::RunSolidHhtStep( + integrator, old, h, p, fail, + [&](const detail::SolidHhtIntegrator& step_integrator, + const detail::SolidHhtPairedPredictor& predictor) + -> Result { + 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 Result::Failure(fail(force.GetStatus())); + candidate.external_force = std::move(force.Value()); + candidate.low.external_force = Vector(dofs.FullDofCount()); + std::array correction{}, fresh_correction{}; + using StepResult = Result>; + const auto report_failure = fail; + const auto record_iteration = [&](const auto& value) { + state.RecordIteration(value); + }; + SolidHhtStep iteration_step( + [&](std::uint32_t iteration) -> StepResult { + const auto fail = [&](const Status& failure) { + return StepResult::Failure(report_failure(failure)); + }; + record.stage = "evaluation"; + record.outcome = "paired-candidate"; + record.evidence = {}; + record.evidence.iteration_index = iteration; + status = + detail::SetSolidHhtRates(step_integrator, old, predictor, 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) { + return StepResult::Success(candidate); + } + record_iteration(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 = + detail::SetSolidHhtRates(step_integrator, old, predictor, h, p, &candidate); + if (!status.IsOk()) + return fail(status); + correction = + ObservedCorrectionMetrics(before, candidate, h, length.Value()); + return StepResult::Success(std::nullopt); + }, + 100U); + const Status solved = iteration_step.Solve(algorithm); + if (!solved.IsOk()) + return Result::Failure(solved); + if (!iteration_step.HasCandidate()) + return Result::Failure( + fail(Failure(domain, "solid-dynamic-nonconvergence", + "No complete paired endpoint was accepted."))); + return Result::Success( + iteration_step.TakeCandidate()); + }, + [&](SolidDynamicAcceptedFrameState&& accepted_candidate) -> Status { + status = state.CommitAcceptedFrame(accepted_candidate); + if (!status.IsOk()) + return fail(status); + record.stage = "accepted"; + record.outcome = "increment"; + state.RecordIteration(record); + return Status::Ok(); + }); + if (!status.IsOk()) return status; } return Status::Ok(); } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/analysis/solid_hht_integrator.h b/src/fesa/analysis/solid_hht_integrator.h new file mode 100644 index 0000000..4a4ea0e --- /dev/null +++ b/src/fesa/analysis/solid_hht_integrator.h @@ -0,0 +1,75 @@ +#ifndef FESA_ANALYSIS_SOLID_HHT_INTEGRATOR_H_ +#define FESA_ANALYSIS_SOLID_HHT_INTEGRATOR_H_ + +#include +#include "analysis/solid_hht_high_low.h" + +namespace fesa::detail { + +/// @brief Predicts and evaluates complete paired mechanical HHT kinematics. +class SolidHhtIntegrator { + public: + virtual ~SolidHhtIntegrator() = default; + virtual Result Predict( + const SolidDynamicAcceptedFrameState& old, double h, + const HhtParameters& parameters) const = 0; + virtual Result EvaluateRates( + const Vector& high, const Vector& low, + const SolidHhtPairedPredictor& predictor, const Vector& old_acceleration, + const Vector& old_acceleration_low, double h, + const HhtParameters& parameters) const = 0; +}; + +/// @brief Retains every high/low physical and auxiliary acceleration channel. +class PairedSolidHhtIntegrator final : public SolidHhtIntegrator { + public: + Result Predict( + const SolidDynamicAcceptedFrameState& old, double h, + const HhtParameters& parameters) const override { + return PredictSolidHhtHighLow(old, h, parameters); + } + Result EvaluateRates( + const Vector& high, const Vector& low, + const SolidHhtPairedPredictor& predictor, const Vector& old_acceleration, + const Vector& old_acceleration_low, double h, + const HhtParameters& parameters) const override { + return EvaluateSolidHhtHighLowRates(high, low, predictor, old_acceleration, + old_acceleration_low, h, parameters); + } +}; + +inline Status SetSolidHhtRates(const SolidHhtIntegrator& integrator, + const SolidDynamicAcceptedFrameState& old, + const SolidHhtPairedPredictor& predictor, double h, + const HhtParameters& p, + SolidDynamicAcceptedFrameState* candidate) { + auto rates = integrator.EvaluateRates( + 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(); +} +/// @brief Preserves predictor failure recording, endpoint evaluation and commit. +/// @note Borrowed callbacks do not escape; only commit may update history. +template +Status RunSolidHhtStep(const SolidHhtIntegrator& integrator, + const SolidDynamicAcceptedFrameState& old, double h, + const HhtParameters& parameters, Failure&& failure, + Evaluate&& evaluate, Commit&& commit) { + auto predictor = integrator.Predict(old, h, parameters); + if (!predictor.HasValue()) return failure(predictor.GetStatus()); + auto pending = evaluate(integrator, predictor.Value()); + if (!pending.HasValue()) return pending.GetStatus(); + return commit(std::move(pending.Value())); +} + +} // namespace fesa::detail +#endif // FESA_ANALYSIS_SOLID_HHT_INTEGRATOR_H_ diff --git a/src/fesa/analysis/solid_static_core.h b/src/fesa/analysis/solid_static_core.h new file mode 100644 index 0000000..139532c --- /dev/null +++ b/src/fesa/analysis/solid_static_core.h @@ -0,0 +1,27 @@ +#ifndef FESA_ANALYSIS_SOLID_STATIC_CORE_H_ +#define FESA_ANALYSIS_SOLID_STATIC_CORE_H_ +#include "fesa/core/status.h" +namespace fesa { +class Domain; +class SolidStaticState; +class ParallelFor; +class LinearSolver; +class TangentSolver; +class EquilibriumAlgorithm; +/// @brief Borrows the production static core's execution/solve/iteration policy. +/// @note Every adapter must outlive ExecuteInto; state owns accepted history. +class SolidStaticCore { + public: + SolidStaticCore(const ParallelFor& parallel_for, LinearSolver& linear_solver, + TangentSolver& tangent_solver, EquilibriumAlgorithm& algorithm) + : parallel_for_(parallel_for), linear_solver_(linear_solver), + tangent_solver_(tangent_solver), algorithm_(algorithm) {} + Status ExecuteInto(const Domain& domain, SolidStaticState& state) const; + private: + const ParallelFor& parallel_for_; + LinearSolver& linear_solver_; + TangentSolver& tangent_solver_; + EquilibriumAlgorithm& algorithm_; +}; +} // namespace fesa +#endif // FESA_ANALYSIS_SOLID_STATIC_CORE_H_ diff --git a/src/fesa/analysis/solid_static_procedure.cpp b/src/fesa/analysis/solid_static_procedure.cpp index eeb3195..35c23bb 100644 --- a/src/fesa/analysis/solid_static_procedure.cpp +++ b/src/fesa/analysis/solid_static_procedure.cpp @@ -1,5 +1,8 @@ #include "fesa/analysis/solid_static_procedure.h" +#include "equilibrium_algorithm.h" +#include "analysis/solid_static_core.h" + #include #include #include @@ -21,6 +24,8 @@ #include "fesa/solvers/linear/linear_solver.h" #include "fesa/solvers/linear/tangent_solver.h" +#include "assembly/ordered_contribution_internal.h" + namespace fesa { namespace { constexpr std::uint32_t kMaximumIterations = 100U; @@ -28,7 +33,7 @@ constexpr double kRoundoffBudget = 512.0 * std::numeric_limits::epsilon(); struct RuntimeSolid { - const Hex8Element* element; + std::unique_ptr element; std::vector scatter; }; struct SolidAssembly { @@ -131,9 +136,8 @@ Result Assemble(const std::vector& solids, const auto& response = responses[e]->Value(); for (std::size_t i = 0U; i < 24U; ++i) { force[solids[e].scatter[i]] += response.internal_force[i]; - for (std::size_t j = 0U; j < 24U; ++j) - contributions.push_back({solids[e].scatter[i], solids[e].scatter[j], - response.tangent(i, j), e, 24U * i + j}); + assembly_internal::AppendLocalMatrixRow( + response.tangent, solids[e].scatter, i, e, contributions); } energy += response.strain_energy; points.insert(points.end(), response.points.begin(), response.points.end()); @@ -283,6 +287,13 @@ SolidStaticProcedureExecutor::SolidStaticProcedureExecutor( Status SolidStaticProcedureExecutor::ExecuteInto( const Domain& domain, SolidStaticState& state) const { + NewtonStepAlgorithm algorithm; + return SolidStaticCore(parallel_for_, linear_solver_, tangent_solver_, algorithm) + .ExecuteInto(domain, state); +} + +Status SolidStaticCore::ExecuteInto( + const Domain& domain, SolidStaticState& state) const { if (state.initialized_) return Failure(domain, "solid-static-observer-not-fresh", "Execution requires a fresh static observer."); @@ -309,14 +320,14 @@ Status SolidStaticProcedureExecutor::ExecuteInto( state = std::move(baseline.Value()); std::vector solids; for (const auto& element : elements) { - const auto* hex = dynamic_cast(element.get()); + auto hex = ElementFactory{}.BindSolidTrial(*element); if (hex == nullptr) return Failure(domain, "invalid-solid-static-element", "Solid STATIC requires full-integration Hex8 kernels.", FailureCategory::kModel); auto scatter = dofs.ElementScatter(hex->DofLayout()); if (!scatter.HasValue()) return scatter.GetStatus(); - solids.push_back({hex, std::move(scatter.Value())}); + solids.push_back({std::move(hex), std::move(scatter.Value())}); } auto stiffness = SparseAssembler::Assemble(view, dofs, parallel_for_); if (!stiffness.HasValue()) return stiffness.GetStatus(); @@ -336,7 +347,7 @@ Status SolidStaticProcedureExecutor::ExecuteInto( // Preserve the required factorization-before-load-assembly lifecycle. auto load = LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs); if (!load.HasValue()) return load.GetStatus(); - auto rhs = LoadAssembler::EffectiveFreeRhs( + auto rhs = constraints.EffectiveFreeRhs( load.Value(), partition.Value().k_fc, dofs.PrescribedValues(), dofs); if (!rhs.HasValue()) return rhs.GetStatus(); Vector free{dofs.FreeDofCount()}; @@ -369,91 +380,128 @@ Status SolidStaticProcedureExecutor::ExecuteInto( for (std::size_t c = 0U; c < dofs.ConstrainedDofCount(); ++c) trial[dofs.ConstrainedDofs()[c]] = increment.load_factor * dofs.PrescribedValues()[c]; - for (std::uint32_t iteration = 1U; iteration <= kMaximumIterations; - ++iteration) { + struct SolidStepCandidate { + SolidStaticFrame frame; SolidStaticIterationRecord record; - record.increment_index = increment.increment_index; - record.load_factor = increment.load_factor; - record.iteration = iteration; - record.stage = "entered"; - const auto fail = [&](const Status& failure) { - record.diagnostic_code = failure.Diagnostics().empty() - ? "solid-static-failure" - : failure.Diagnostics()[0].code; - record.failure_reason = failure.Diagnostics().empty() - ? "Static trial failed." - : failure.Diagnostics()[0].message; - state.iterations_.push_back(record); - return failure; - }; - record.stage = "trial-recovery"; - auto assembly = Assemble(solids, dofs, trial, true, parallel_for_); - if (!assembly.HasValue()) return fail(assembly.GetStatus()); - SolidStaticFrame frame = state.committed_; - frame.balance = {}; - frame.global_equilibrium = {}; - frame.frame_index = - static_cast(state.accepted_frames_.size()); - frame.increment_index = increment.increment_index; - frame.load_factor = increment.load_factor; - frame.iteration_count = iteration; - frame.is_factor_one = increment.is_factor_one; - frame.displacement = trial; - frame.internal_force = std::move(assembly.Value().force); - frame.external_force = base_load.Value(); - frame.external_force.Scale(increment.load_factor); - frame.solid_results = std::move(assembly.Value().points); - frame.physical_strain_energy = assembly.Value().energy; - auto status = CompleteFrame(domain, dofs, true, stiffness.Value(), frame); - if (!status.IsOk()) return fail(status); - record.stage = "tangent-partition"; - auto partition = constraints.Partition(assembly.Value().tangent, dofs); - if (!partition.HasValue()) return fail(partition.GetStatus()); - Vector correction{dofs.FreeDofCount()}; - // Empty free is vacuous; prescribed geometry/recovery above still runs. - if (dofs.FreeDofCount() != 0U) { - record.stage = "tangent-factorization"; - status = tangent_solver_.FactorizeFresh(partition.Value().k_ff); - if (!status.IsOk()) return fail(status); - auto rhs = constraints.GatherFree(frame.residual, dofs); - rhs.Scale(-1.0); - record.stage = "correction-solve"; - status = tangent_solver_.SolveCorrection(rhs, correction); - if (!status.IsOk()) return fail(status); - } - Vector current_increment{dofs.FreeDofCount()}; - for (std::size_t f = 0U; f < dofs.FreeDofCount(); ++f) { - const auto full = dofs.FreeDofs()[f]; - current_increment[f] = trial[full] - start[full]; - } - record.stage = "convergence"; - auto convergence = EvaluateSolidStaticConvergence( - constraints.GatherFree(frame.residual, dofs), - constraints.GatherFree(frame.internal_force, dofs), - constraints.GatherFree(frame.external_force, dofs), correction, - current_increment); - if (!convergence.HasValue()) return fail(convergence.GetStatus()); - record.force_residual = convergence.Value().force_residual; - record.translation_correction = - convergence.Value().translation_correction; - record.convergence_evaluated = true; - if (record.force_residual.passed && - record.translation_correction.passed) { - record.stage = "commit"; - status = state.CommitAcceptedFrame(frame); - if (!status.IsOk()) return fail(status); - record.accepted = true; - state.iterations_.push_back(record); - break; - } - if (iteration == kMaximumIterations) - return fail(Failure(domain, "solid-static-nonconvergence", - "Solid STATIC exceeded 100 Newton iterations.")); - for (std::size_t f = 0U; f < dofs.FreeDofCount(); ++f) - trial[dofs.FreeDofs()[f]] += correction[f]; - record.stage = "continue"; - state.iterations_.push_back(record); + }; + const auto observe_iteration = + [&](const SolidStaticIterationRecord& record) { + state.iterations_.push_back(record); + }; + SolidFullNewtonStep iteration_step( + [&](std::uint32_t iteration) + -> Result> { + SolidStaticIterationRecord record; + record.increment_index = increment.increment_index; + record.load_factor = increment.load_factor; + record.iteration = iteration; + record.stage = "entered"; + const auto fail = [&](const Status& failure) { + record.diagnostic_code = failure.Diagnostics().empty() + ? "solid-static-failure" + : failure.Diagnostics()[0].code; + record.failure_reason = failure.Diagnostics().empty() + ? "Static trial failed." + : failure.Diagnostics()[0].message; + observe_iteration(record); + return Result>::Failure(failure); + }; + record.stage = "trial-recovery"; + auto assembly = Assemble(solids, dofs, trial, true, parallel_for_); + if (!assembly.HasValue()) + return fail(assembly.GetStatus()); + SolidStaticFrame frame = state.committed_; + frame.balance = {}; + frame.global_equilibrium = {}; + frame.frame_index = + static_cast(state.accepted_frames_.size()); + frame.increment_index = increment.increment_index; + frame.load_factor = increment.load_factor; + frame.iteration_count = iteration; + frame.is_factor_one = increment.is_factor_one; + frame.displacement = trial; + frame.internal_force = std::move(assembly.Value().force); + frame.external_force = base_load.Value(); + frame.external_force.Scale(increment.load_factor); + frame.solid_results = std::move(assembly.Value().points); + frame.physical_strain_energy = assembly.Value().energy; + auto status = + CompleteFrame(domain, dofs, true, stiffness.Value(), frame); + if (!status.IsOk()) + return fail(status); + record.stage = "tangent-partition"; + auto partition = + constraints.Partition(assembly.Value().tangent, dofs); + if (!partition.HasValue()) + return fail(partition.GetStatus()); + Vector correction{dofs.FreeDofCount()}; + // Empty free is vacuous; prescribed geometry/recovery above still + // runs. + if (dofs.FreeDofCount() != 0U) { + record.stage = "tangent-factorization"; + status = tangent_solver_.FactorizeFresh(partition.Value().k_ff); + if (!status.IsOk()) + return fail(status); + auto rhs = constraints.GatherFree(frame.residual, dofs); + rhs.Scale(-1.0); + record.stage = "correction-solve"; + status = tangent_solver_.SolveCorrection(rhs, correction); + if (!status.IsOk()) + return fail(status); + } + Vector current_increment{dofs.FreeDofCount()}; + for (std::size_t f = 0U; f < dofs.FreeDofCount(); ++f) { + const auto full = dofs.FreeDofs()[f]; + current_increment[f] = trial[full] - start[full]; + } + record.stage = "convergence"; + auto convergence = EvaluateSolidStaticConvergence( + constraints.GatherFree(frame.residual, dofs), + constraints.GatherFree(frame.internal_force, dofs), + constraints.GatherFree(frame.external_force, dofs), correction, + current_increment); + if (!convergence.HasValue()) + return fail(convergence.GetStatus()); + record.force_residual = convergence.Value().force_residual; + record.translation_correction = + convergence.Value().translation_correction; + record.convergence_evaluated = true; + if (record.force_residual.passed && + record.translation_correction.passed) { + record.stage = "commit"; + return Result>::Success( + SolidStepCandidate{std::move(frame), record}); + } + if (iteration == kMaximumIterations) + return fail( + Failure(domain, "solid-static-nonconvergence", + "Solid STATIC exceeded 100 Newton iterations.")); + for (std::size_t f = 0U; f < dofs.FreeDofCount(); ++f) + trial[dofs.FreeDofs()[f]] += correction[f]; + record.stage = "continue"; + observe_iteration(record); + return Result>::Success( + std::nullopt); + }, + kMaximumIterations); + const Status solved = iteration_step.Solve(algorithm_); + if (!solved.IsOk()) + return solved; + auto candidate = iteration_step.TakeCandidate(); + auto& record = candidate.record; + const Status committed = state.CommitAcceptedFrame(candidate.frame); + if (!committed.IsOk()) { + record.diagnostic_code = committed.Diagnostics().empty() + ? "solid-static-failure" + : committed.Diagnostics()[0].code; + record.failure_reason = committed.Diagnostics().empty() + ? "Static trial failed." + : committed.Diagnostics()[0].message; + observe_iteration(record); + return committed; } + record.accepted = true; + observe_iteration(record); } return Status::Ok(); } diff --git a/src/fesa/analysis/solid_static_state.cpp b/src/fesa/analysis/solid_static_state.cpp index 02bfb52..ae9b999 100644 --- a/src/fesa/analysis/solid_static_state.cpp +++ b/src/fesa/analysis/solid_static_state.cpp @@ -37,10 +37,9 @@ double InfinityNorm(const Vector& values) { return norm; } /// @brief Compares immutable semantic and raw source identities. -bool SameIdentity(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; +bool SameIdentity(const SourceEntityId& a, + const SourceEntityId& b) { + return fesa::SourceIdentityEquals(a, b); } /// @brief Builds one raw criterion with the approved branch and physical unit. NewtonChannelDecision Decision(const char* name, double numerator, @@ -119,7 +118,7 @@ Result SolidStaticState::Create(const Domain& domain, auto element = ElementFactory{}.Create(domain.Elements()[e], domain); if (!element.HasValue()) return Result::Failure(element.GetStatus()); - const auto* hex = dynamic_cast(element.Value().get()); + auto hex = ElementFactory{}.BindSolidTrial(*element.Value()); if (hex == nullptr) return Result::Failure(StateFailure( "invalid-solid-static-state", "Static state requires Hex8 kernels.")); diff --git a/src/fesa/analysis/thermal_analysis_state.cpp b/src/fesa/analysis/thermal_analysis_state.cpp index 1e2e034..f229d93 100644 --- a/src/fesa/analysis/thermal_analysis_state.cpp +++ b/src/fesa/analysis/thermal_analysis_state.cpp @@ -8,6 +8,9 @@ #include #include +#include "fesa/assembly/thermal_assembler.h" +#include "math/finite_values.h" + namespace fesa { namespace { @@ -23,9 +26,7 @@ Status Failure(const std::string& message) { /// @brief Checks every stored scalar without imposing a temperature sign bound. bool Finite(const Vector& vector) { - for (std::size_t i = 0; i < vector.Size(); ++i) - if (!std::isfinite(vector[i])) return false; - return true; + return math_internal::IsFinite(vector); } /// @brief Compares prescribed row identities without conflating node and row @@ -64,8 +65,8 @@ Result ThermalAnalysisState::Create( for (const auto node : state.node_order_) state.node_source_ids_.push_back(domain.Nodes()[node].source_id); for (std::size_t index = 0; index < state.element_order_.size(); ++index) { - state.element_source_ids_.push_back(assembly.Kernels()[index].SourceId()); - const auto frame = assembly.Kernels()[index].Recover({}); + state.element_source_ids_.push_back(assembly.Elements()[index]->DofLayout().source_id); + const auto frame = assembly.Elements()[index]->Recover({}); if (!frame.HasValue()) return Result::Failure(frame.GetStatus()); state.local_frames_.push_back(frame.Value().frames); diff --git a/src/fesa/analysis/thermal_transient_algebra.cpp b/src/fesa/analysis/thermal_transient_algebra.cpp index 2784d43..d323d4a 100644 --- a/src/fesa/analysis/thermal_transient_algebra.cpp +++ b/src/fesa/analysis/thermal_transient_algebra.cpp @@ -7,8 +7,10 @@ #include #include +#include "fesa/assembly/thermal_assembler.h" #include "thermal_transient_bounds.h" #include "math/dense_blas_internal.h" +#include "math/finite_values.h" namespace fesa::thermal_transient_internal { namespace { @@ -35,9 +37,7 @@ Status Failure(const std::string& message) { } /// @brief Checks every scalar while preserving signed temperatures and powers. bool Finite(const Vector& vector) { - for (std::size_t i = 0; i < vector.Size(); ++i) - if (!std::isfinite(vector[i])) return false; - return true; + return math_internal::IsFinite(vector); } /// @brief Checks aligned full operators and their coefficient-error storage. bool SamePattern(const SparseMatrix& a, const SparseMatrix& b) { @@ -500,6 +500,16 @@ Status RunThermalHistory(const ThermalAssembly& assembly, const DofManager& dofs, ThermalAnalysisState& state, LinearSolver& solver, const ThermalRecovery& recovery, std::vector* evidence) { + const BackwardEulerHeatIntegrator integrator; + return RunThermalHistory(assembly, dofs, state, solver, integrator, recovery, + evidence); +} + +Status RunThermalHistory(const ThermalAssembly& assembly, + const DofManager& dofs, ThermalAnalysisState& state, + LinearSolver& solver, const HeatStepIntegrator& integrator, + const ThermalRecovery& recovery, + std::vector* evidence) { if (!dofs.ValidateInvariants().IsOk() || dofs.Profile() != DofProfile::kThermal || dofs.FullDofCount() != state.CommittedState().temperature.Size() || @@ -517,19 +527,21 @@ Status RunThermalHistory(const ThermalAssembly& assembly, i <= count; ++i) { const auto frame = state.TimeGrid().Frame(static_cast(i)); if (!frame.HasValue()) return frame.GetStatus(); - auto candidate = SolveThermalIncrement(system, partition, - state.CommittedState().temperature, - frame.Value().delta, solver, cache); - if (!candidate.HasValue()) return candidate.GetStatus(); - auto flux = recovery(candidate.Value().temperature); - if (!flux.HasValue()) return flux.GetStatus(); - auto completed = state.MakeCompletedFrame( - static_cast(i), std::move(candidate.Value().temperature), - std::move(candidate.Value().reaction), std::move(flux.Value())); - if (!completed.HasValue()) return completed.GetStatus(); - const auto committed = state.CommitAcceptedFrame(completed.Value()); - if (!committed.IsOk()) return committed; - if (evidence) evidence->push_back(candidate.Value().evidence); + const Status step_status = RunThermalStep( + integrator, system, partition, state.CommittedState().temperature, + frame.Value().delta, solver, cache, recovery, + [&](ThermalStepCandidate&& candidate, + std::vector&& flux) -> Status { + auto completed = state.MakeCompletedFrame( + static_cast(i), std::move(candidate.temperature), + std::move(candidate.reaction), std::move(flux)); + if (!completed.HasValue()) return completed.GetStatus(); + const auto committed = state.CommitAcceptedFrame(completed.Value()); + if (!committed.IsOk()) return committed; + if (evidence) evidence->push_back(candidate.evidence); + return Status::Ok(); + }); + if (!step_status.IsOk()) return step_status; } return Status::Ok(); } diff --git a/src/fesa/analysis/thermal_transient_algebra.h b/src/fesa/analysis/thermal_transient_algebra.h index 5b05ce7..a361aea 100644 --- a/src/fesa/analysis/thermal_transient_algebra.h +++ b/src/fesa/analysis/thermal_transient_algebra.h @@ -4,8 +4,10 @@ #include #include #include +#include #include "fesa/analysis/thermal_analysis_state.h" +#include "fesa/math/sparse_matrix.h" #include "fesa/solvers/linear/linear_solver.h" namespace fesa::thermal_transient_internal { @@ -84,11 +86,52 @@ Result SolveThermalIncrement( const Vector& old_temperature, double delta, LinearSolver& solver, ThermalFactorization& factorization); +/// @brief Solves one thermal increment without committing accepted history. +class HeatStepIntegrator { +public: + virtual ~HeatStepIntegrator() = default; + virtual Result + SolveIncrement(const ThermalSystem &system, const ThermalPartition &partition, + const Vector &old_temperature, double delta, + LinearSolver &solver, + ThermalFactorization &factorization) const = 0; +}; + +/// @brief Preserves backward Euler actual-delta and factorization reuse policy. +class BackwardEulerHeatIntegrator final : public HeatStepIntegrator { +public: + Result + SolveIncrement(const ThermalSystem &system, const ThermalPartition &partition, + const Vector &old_temperature, double delta, + LinearSolver &solver, + ThermalFactorization &factorization) const override { + return SolveThermalIncrement(system, partition, old_temperature, delta, + solver, factorization); + } +}; + /// @brief Provides an internal recovery seam for the actual history commit /// path. using ThermalRecovery = std::function>( const Vector& temperature)>; +/// @brief Runs the actual solve/recovery/commit sequence without owning history. +/// @note Commit receives a validated unpublished candidate only after recovery. +template +Status RunThermalStep(const HeatStepIntegrator& integrator, + const ThermalSystem& system, + const ThermalPartition& partition, + const Vector& old_temperature, double delta, + LinearSolver& solver, ThermalFactorization& factorization, + const ThermalRecovery& recovery, Commit&& commit) { + auto candidate = integrator.SolveIncrement(system, partition, old_temperature, + delta, solver, factorization); + if (!candidate.HasValue()) return candidate.GetStatus(); + auto flux = recovery(candidate.Value().temperature); + if (!flux.HasValue()) return flux.GetStatus(); + return commit(std::move(candidate.Value()), std::move(flux.Value())); +} + /// @brief Advances an existing raw/accepted history, preserving it on every /// failure. /// @note Recovery runs after algebra validation and before atomic commit. @@ -101,5 +144,13 @@ Status RunThermalHistory(const ThermalAssembly& assembly, LinearSolver& solver, const ThermalRecovery& recovery, std::vector* evidence = nullptr); +/// @brief Borrows a caller-selected integrator for the complete history core. +/// @note Integrator and other referenced adapters outlive the synchronous call. +Status RunThermalHistory(const ThermalAssembly& assembly, + const DofManager& dofs, ThermalAnalysisState& state, + LinearSolver& solver, const HeatStepIntegrator& integrator, + const ThermalRecovery& recovery, + std::vector* evidence = nullptr); + } // namespace fesa::thermal_transient_internal #endif // FESA_ANALYSIS_THERMAL_TRANSIENT_ALGEBRA_H_ diff --git a/src/fesa/analysis/transient_heat_procedure.cpp b/src/fesa/analysis/transient_heat_procedure.cpp index b8cfdf1..ff819cc 100644 --- a/src/fesa/analysis/transient_heat_procedure.cpp +++ b/src/fesa/analysis/transient_heat_procedure.cpp @@ -1,6 +1,7 @@ #include "fesa/analysis/transient_heat_procedure.h" #include +#include "fesa/assembly/thermal_assembler.h" #include "thermal_transient_algebra.h" diff --git a/src/fesa/app/fesa_application.cpp b/src/fesa/app/fesa_application.cpp index f160877..658c460 100644 --- a/src/fesa/app/fesa_application.cpp +++ b/src/fesa/app/fesa_application.cpp @@ -12,6 +12,9 @@ #include "fesa/analysis/nonlinear_static_procedure.h" #include "fesa/analysis/prepared_analysis.h" #include "fesa/analysis/solid_frequency_procedure.h" +#include "fesa/analysis/solid_hht_dynamic_procedure.h" +#include "fesa/analysis/solid_static_procedure.h" +#include "fesa/analysis/transient_heat_procedure.h" #include "fesa/app/output_paths.h" #include "fesa/assembly/parallel_for.h" #include "fesa/core/diagnostic.h" @@ -21,6 +24,7 @@ #include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h" #include "fesa/io/hdf5/solid_hdf5_results_writer.h" #include "fesa/io/hdf5/thermal_hdf5_results_writer.h" +#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" #include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h" @@ -84,6 +88,7 @@ int FesaApplication::Run(const std::vector& arguments) { } TbbParallelFor parallel_for; + const MklFeastEigenSolver eigen_solver; MklPardisoSolver linear_solver; MklPardisoTangentSolver tangent_solver; Hdf5ResultsWriter linear_writer; @@ -105,23 +110,12 @@ int FesaApplication::Run(const std::vector& arguments) { if (!prepared.HasValue()) { status = prepared.GetStatus(); } else { - LinearStaticProcedureExecutor linear_executor(parallel_for, linear_solver); - NonlinearStaticProcedureExecutor nonlinear_executor(parallel_for, - tangent_solver); - const LinearFrequencyProcedureExecutor frequency_executor; - const HhtDynamicProcedureExecutor dynamic_executor( - parallel_for, linear_solver, tangent_solver); - const TransientHeatProcedureExecutor thermal_executor(parallel_for, - linear_solver); - const SolidStaticProcedureExecutor solid_static(parallel_for, linear_solver, - tangent_solver); - const SolidFrequencyProcedureExecutor solid_frequency; - const SolidHhtDynamicProcedureExecutor solid_dynamic( - parallel_for, linear_solver, tangent_solver); - Result outcome = ExecutePreparedAnalysis( - prepared.Value().domain, linear_executor, nonlinear_executor, - frequency_executor, log, &dynamic_executor, &thermal_executor, - &solid_static, &solid_frequency, &solid_dynamic); + auto procedure = CreateAnalysisProcedure( + prepared.Value().domain, parallel_for, linear_solver, tangent_solver, + eigen_solver, log); + Result outcome = procedure.HasValue() + ? ExecutePreparedAnalysis(prepared.Value().domain, *procedure.Value()) + : Result::Failure(procedure.GetStatus()); if (!outcome.HasValue()) { status = outcome.GetStatus(); } else { diff --git a/src/fesa/assembly/load_assembler.cpp b/src/fesa/assembly/load_assembler.cpp index caa0563..b74d057 100644 --- a/src/fesa/assembly/load_assembler.cpp +++ b/src/fesa/assembly/load_assembler.cpp @@ -11,7 +11,8 @@ #include "fesa/constraints/essential_constraint_policy.h" #include "fesa/loads/load.h" #include "fesa/model/source_target_resolver.h" -#include "math/dense_blas_internal.h" + +#include "assembly/ordered_contribution_internal.h" namespace fesa { namespace { @@ -27,20 +28,6 @@ Status LoadFailure(const std::string& code, const SourceLocation& location, {{Severity::kError, code, location, keyword, identity, message}}); } -Status ValidateFiniteVector(const Vector& values, - const SourceLocation& location, - const std::string& identity) { - for (std::size_t index = 0U; index < values.Size(); ++index) { - if (!std::isfinite(values[index])) { - return LoadFailure("nonfinite-load-value", location, "LOAD_ASSEMBLER", - identity + ":" + std::to_string(index), - "Load and prescribed displacement vectors must " - "contain finite values."); - } - } - return Status::Ok(); -} - Status ValidateShellMoments(const Domain& domain, const Vector& full_load) { if (domain.ShellElements().Empty()) { return Status::Ok(); @@ -224,14 +211,13 @@ Result LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model, for (const auto& contributions : contributions_by_load) { for (const auto& contribution : contributions) { const std::size_t full_dof = contribution.full_dof_index; - const double accumulated = full_load[full_dof] + contribution.value; - if (!std::isfinite(accumulated)) { + if (!assembly_internal::AccumulateFiniteLoad( + full_load, full_dof, contribution.value)) { return Result::Failure(LoadFailure( "nonfinite-load-accumulation", model.Step().Location(), "LOAD_ASSEMBLER", std::to_string(full_dof), "Source-order load accumulation produced a nonfinite value.")); } - full_load[full_dof] = accumulated; } } const Status shell_moment_status = ValidateShellMoments(domain, full_load); @@ -245,101 +231,8 @@ Result LoadAssembler::EffectiveFreeRhs(const Vector& full_load, const SparseMatrix& kfc, const Vector& prescribed_values, const DofManager& dofs) { - const SourceLocation location{{}, 0U}; - if (dofs.FullDofCount() != full_load.Size()) { - return Result::Failure( - LoadFailure("invalid-load-dimensions", location, "LOAD_ASSEMBLER", - std::to_string(dofs.FullDofCount()), - "The DofManager full dimension must match the full load.")); - } - const Status dof_status = dofs.ValidateInvariants(); - if (!dof_status.IsOk()) { - return Result::Failure(dof_status); - } - if (kfc.Rows() != dofs.FreeDofCount() || - kfc.Columns() != dofs.ConstrainedDofCount() || - prescribed_values.Size() != dofs.ConstrainedDofCount()) { - return Result::Failure(LoadFailure( - "invalid-load-dimensions", location, "LOAD_ASSEMBLER", - std::to_string(kfc.Rows()) + "x" + std::to_string(kfc.Columns()), - "Kfc rows/columns and prescribed values must match free/constrained " - "order.")); - } - const Status matrix_status = kfc.Validate(); - if (!matrix_status.IsOk()) { - return Result::Failure(matrix_status); - } - const Status load_status = - ValidateFiniteVector(full_load, location, "full-load"); - if (!load_status.IsOk()) { - return Result::Failure(load_status); - } - const Status prescribed_status = - ValidateFiniteVector(prescribed_values, location, "prescribed-values"); - if (!prescribed_status.IsOk()) { - return Result::Failure(prescribed_status); - } - - Vector entry_coefficients{kfc.Values().size()}; - Vector entry_prescribed{kfc.Values().size()}; - for (std::size_t position = 0U; position < kfc.Values().size(); ++position) { - entry_coefficients[position] = kfc.Values()[position]; - entry_prescribed[position] = - prescribed_values[kfc.ColumnIndices()[position]]; - } - Vector entry_products{0U}; - try { - entry_products = dense_blas_internal::MultiplyDiagonal(entry_coefficients, - entry_prescribed); - } catch (const std::length_error&) { - return Result::Failure(LoadFailure( - "load-backend-dimension-overflow", location, "LOAD_ASSEMBLER", "Kfc", - "Prescribed displacement products exceed the backend integer range.")); - } catch (const std::invalid_argument&) { - return Result::Failure(LoadFailure( - "invalid-load-product", location, "LOAD_ASSEMBLER", "Kfc", - "Prescribed displacement product inputs must be finite and compatible.")); - } - - // MKL computes independent entries; ordered CSR reduction below retains - // cancellation and the first product/row-sum overflow diagnostic. - Vector correction{kfc.Rows()}; - for (std::size_t row = 0U; row < kfc.Rows(); ++row) { - double sum = 0.0; - for (std::size_t position = kfc.RowOffsets()[row]; - position < kfc.RowOffsets()[row + 1U]; ++position) { - const double product = entry_products[position]; - if (!std::isfinite(product)) { - return Result::Failure(LoadFailure( - "nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", - std::to_string(row), - "Kfc times prescribed displacement produced a nonfinite product.")); - } - sum += product; - if (!std::isfinite(sum)) { - return Result::Failure(LoadFailure( - "nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", - std::to_string(row), - "Kfc times prescribed displacement produced a nonfinite row sum.")); - } - } - correction[row] = sum; - } - - Vector rhs = EssentialConstraintPolicy{}.GatherFree(full_load, dofs); - // The constrained vector is already in DofManager order, so this is the - // approved elimination equation rhs = Ff - Kfc*dc without reordering dc. - for (std::size_t row = 0U; row < rhs.Size(); ++row) { - const double value = rhs[row] - correction[row]; - if (!std::isfinite(value)) { - return Result::Failure( - LoadFailure("nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", - std::to_string(row), - "Effective RHS subtraction produced a nonfinite value.")); - } - rhs[row] = value; - } - return Result::Success(std::move(rhs)); + return EssentialConstraintPolicy{}.EffectiveFreeRhs( + full_load, kfc, prescribed_values, dofs); } } // namespace fesa diff --git a/src/fesa/assembly/ordered_contribution_internal.h b/src/fesa/assembly/ordered_contribution_internal.h new file mode 100644 index 0000000..c22e25a --- /dev/null +++ b/src/fesa/assembly/ordered_contribution_internal.h @@ -0,0 +1,58 @@ +#ifndef FESA_ASSEMBLY_ORDERED_CONTRIBUTION_INTERNAL_H_ +#define FESA_ASSEMBLY_ORDERED_CONTRIBUTION_INTERNAL_H_ + +#include +#include +#include + +#include "fesa/math/matrix.h" +#include "fesa/math/sparse_matrix.h" +#include "fesa/math/vector.h" + +namespace fesa::assembly_internal { + +/// @brief Appends one validated local entry without changing its stable order. +/// @note Caller owns shape/scatter validation and diagnostic selection. Retains +/// every structural zero; never writes shared global storage from a worker. +template +void AppendLocalMatrixEntry(const Scatter& scatter, std::size_t row, + std::size_t column, double value, + std::size_t source_order, + std::vector& contributions) { + contributions.push_back({scatter[row], scatter[column], value, source_order, + row * scatter.size() + column}); +} + +/// @brief Appends a validated matrix row in the original local-column order. +template +void AppendLocalMatrixRow(const Matrix& values, const Scatter& scatter, + std::size_t row, std::size_t source_order, + std::vector& contributions) { + for (std::size_t column = 0U; column < scatter.size(); ++column) { + AppendLocalMatrixEntry(scatter, row, column, values(row, column), + source_order, contributions); + } +} + +/// @brief Appends a validated matrix in fixed local row/column order. +template +void AppendLocalMatrix(const Matrix& values, const Scatter& scatter, + std::size_t source_order, + std::vector& contributions) { + for (std::size_t row = 0U; row < scatter.size(); ++row) { + AppendLocalMatrixRow(values, scatter, row, source_order, contributions); + } +} + +/// @brief Adds one full-space load entry, committing only a finite candidate. +/// @note Caller validates the index and keeps source-order error provenance. +inline bool AccumulateFiniteLoad(Vector& full, std::size_t index, double value) { + const double accumulated = full[index] + value; + if (!std::isfinite(value) || !std::isfinite(accumulated)) return false; + full[index] = accumulated; + return true; +} + +} // namespace fesa::assembly_internal + +#endif // FESA_ASSEMBLY_ORDERED_CONTRIBUTION_INTERNAL_H_ diff --git a/src/fesa/assembly/sparse_assembler.cpp b/src/fesa/assembly/sparse_assembler.cpp index 61face6..8cb7146 100644 --- a/src/fesa/assembly/sparse_assembler.cpp +++ b/src/fesa/assembly/sparse_assembler.cpp @@ -15,6 +15,8 @@ #include "fesa/fem/dof_manager.h" #include "fesa/model/domain.h" +#include "assembly/ordered_contribution_internal.h" + namespace fesa { namespace { @@ -26,15 +28,8 @@ Status AssemblyFailure(const std::string& code, const SourceLocation& location, {{Severity::kError, code, location, "*ELEMENT", identity, message}}); } -bool SameSourceIdentity(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; -} - bool SameLayout(const ElementDofLayout& left, const ElementDofLayout& right) { - return SameSourceIdentity(left.source_id, right.source_id) && + return SourceIdentityEquals(left.source_id, right.source_id) && left.node_indices == right.node_indices && left.components_per_node == right.components_per_node; } @@ -80,17 +75,8 @@ Result SparseAssembler::Assemble( auto& buffer = local_buffers[element_order]; buffer.reserve(local_dof_count * local_dof_count); - for (std::size_t local_row = 0U; local_row < local_dof_count; ++local_row) { - for (std::size_t local_column = 0U; local_column < local_dof_count; - ++local_column) { - const std::size_t local_order = - local_row * local_dof_count + local_column; - buffer.push_back({scatter.Value()[local_row], - scatter.Value()[local_column], - contribution.values(local_row, local_column), - element_order, local_order}); - } - } + assembly_internal::AppendLocalMatrix( + contribution.values, scatter.Value(), element_order, buffer); }); for (std::size_t element_order = 0U; element_order < local_failures.size(); diff --git a/src/fesa/assembly/surface_pressure_assembler.cpp b/src/fesa/assembly/surface_pressure_assembler.cpp index aafe1a6..e5db7b0 100644 --- a/src/fesa/assembly/surface_pressure_assembler.cpp +++ b/src/fesa/assembly/surface_pressure_assembler.cpp @@ -13,6 +13,8 @@ #include "fesa/elements/element_definition.h" #include "fesa/model/domain.h" +#include "assembly/ordered_contribution_internal.h" + namespace fesa { namespace { @@ -250,14 +252,13 @@ Result AssembleSurfacePressure( for (std::size_t local_row = 0U; local_row < kFacetDofCount; ++local_row) { const std::size_t full_row = task.scatter[local_row]; const double force = contribution.force[local_row]; - const double accumulated = full_force[full_row] + force; - if (!std::isfinite(force) || !std::isfinite(accumulated)) { + if (!assembly_internal::AccumulateFiniteLoad(full_force, full_row, + force)) { return Result::Failure(Failure( FailureCategory::kSolver, task.input.location, "nonfinite-pressure-accumulation", task.input.entity_identity, "Stable pressure force reduction produced a nonfinite value.")); } - full_force[full_row] = accumulated; for (std::size_t local_column = 0U; local_column < kFacetDofCount; ++local_column) { const double tangent = contribution.external_tangent[ @@ -268,12 +269,9 @@ Result AssembleSurfacePressure( "nonfinite-pressure-tangent", task.input.entity_identity, "A pressure local tangent contains a nonfinite value.")); } - tangent_contributions.push_back( - {full_row, - task.scatter[local_column], - tangent, - task_index, - local_row * kFacetDofCount + local_column}); + assembly_internal::AppendLocalMatrixEntry( + task.scatter, local_row, local_column, tangent, task_index, + tangent_contributions); } } } diff --git a/src/fesa/assembly/thermal_assembler.cpp b/src/fesa/assembly/thermal_assembler.cpp index f67e755..8fc05f2 100644 --- a/src/fesa/assembly/thermal_assembler.cpp +++ b/src/fesa/assembly/thermal_assembler.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,8 @@ #include "fesa/elements/thermal_element_layout.h" +#include "assembly/ordered_contribution_internal.h" + namespace fesa { namespace { @@ -92,6 +95,7 @@ struct SumInterval { struct LocalContribution { std::optional kernel; + std::unique_ptr element; std::array scatter{}; std::vector conductivity; std::vector capacity; @@ -105,8 +109,8 @@ Result AssembleError(const SparseMatrix& matrix, bool capacity) { std::vector sums(matrix.Values().size()); for (const auto& contribution : local) { - const Matrix& error = capacity ? contribution.kernel->CapacityError() - : contribution.kernel->ConductivityError(); + const Matrix& error = capacity ? contribution.element->CapacityError() + : contribution.element->ConductivityError(); const auto& values = capacity ? contribution.capacity : contribution.conductivity; for (const auto& value : values) { @@ -215,16 +219,21 @@ Result AssembleThermal(const AnalysisModel& model, std::copy(scatter.Value().begin(), scatter.Value().end(), output.scatter.begin()); output.kernel = std::move(kernel.Value()); + // local is fully sized before workers run; this optional cannot move + // until every local matrix/error consumer has finished. + output.element = std::make_unique(*output.kernel); output.conductivity.reserve(400); output.capacity.reserve(400); + const Matrix& conductivity_matrix = output.element->Conductivity(); + const Matrix& capacity_matrix = output.element->Capacity(); for (std::size_t i = 0; i < 20; ++i) for (std::size_t j = 0; j < 20; ++j) { - output.conductivity.push_back({output.scatter[i], output.scatter[j], - output.kernel->Conductivity()(i, j), - index, 20 * i + j}); - output.capacity.push_back({output.scatter[i], output.scatter[j], - output.kernel->Capacity()(i, j), index, - 20 * i + j}); + assembly_internal::AppendLocalMatrixEntry( + output.scatter, i, j, conductivity_matrix(i, j), index, + output.conductivity); + assembly_internal::AppendLocalMatrixEntry( + output.scatter, i, j, capacity_matrix(i, j), index, + output.capacity); } }); std::vector conductivity; @@ -310,7 +319,11 @@ ThermalAssembly::ThermalAssembly( input_error_{std::move(input_error)}, element_order_{std::move(element_order)}, kernels_{std::move(kernels)}, - scatters_{std::move(scatters)} {} + scatters_{std::move(scatters)} { + elements_.reserve(kernels_.size()); + for (const auto& kernel : kernels_) + elements_.push_back(std::make_unique(kernel)); +} const SparseMatrix& ThermalAssembly::Conductivity() const noexcept { return conductivity_; } @@ -330,6 +343,10 @@ const Vector& ThermalAssembly::HeatInputError() const noexcept { const std::vector& ThermalAssembly::ElementOrder() const noexcept { return element_order_; } +const std::vector>& +ThermalAssembly::Elements() const noexcept { + return elements_; +} const std::vector& ThermalAssembly::Kernels() const noexcept { return kernels_; } @@ -358,7 +375,7 @@ Result> ThermalAssembly::RecoverFlux( std::array local{}; for (std::size_t p = 0; p < 20; ++p) local[p] = temperature[scatters_[index][p]]; - const auto result = kernels_[index].Recover(local); + const auto result = elements_[index]->Recover(local); if (!result.HasValue()) { status[index] = result.GetStatus(); return; diff --git a/src/fesa/constraints/essential_constraint_policy.cpp b/src/fesa/constraints/essential_constraint_policy.cpp index 5903d0d..4040323 100644 --- a/src/fesa/constraints/essential_constraint_policy.cpp +++ b/src/fesa/constraints/essential_constraint_policy.cpp @@ -1,5 +1,6 @@ #include "fesa/constraints/essential_constraint_policy.h" +#include #include #include #include @@ -7,6 +8,7 @@ #include #include "fesa/fem/dof_manager.h" +#include "math/dense_blas_internal.h" namespace fesa { namespace { @@ -22,6 +24,29 @@ Status ConstraintFailure(const std::string& code, const std::string& identity, message}}); } +// Preserve the established elimination diagnostics at the compatibility boundary. +Status LoadFailure(const std::string& code, const SourceLocation& location, + const std::string& keyword, const std::string& identity, + const std::string& message) { + return Status::Failure( + FailureCategory::kModel, + {{Severity::kError, code, location, keyword, identity, message}}); +} + +Status ValidateFiniteVector(const Vector& values, + const SourceLocation& location, + const std::string& identity) { + for (std::size_t index = 0U; index < values.Size(); ++index) { + if (!std::isfinite(values[index])) { + return LoadFailure("nonfinite-load-value", location, "LOAD_ASSEMBLER", + identity + ":" + std::to_string(index), + "Load and prescribed displacement vectors must " + "contain finite values."); + } + } + return Status::Ok(); +} + /// @brief Extracts one partition without changing the supplied DOF order. Result ExtractBlock(const SparseMatrix& full, const std::vector& row_dofs, @@ -156,4 +181,104 @@ Vector EssentialConstraintPolicy::ReconstructFull( return full; } +Result EssentialConstraintPolicy::EffectiveFreeRhs( + const Vector& full_load, const SparseMatrix& kfc, + const Vector& prescribed_values, const DofManager& dofs) const { + const SourceLocation location{{}, 0U}; + if (dofs.FullDofCount() != full_load.Size()) { + return Result::Failure( + LoadFailure("invalid-load-dimensions", location, "LOAD_ASSEMBLER", + std::to_string(dofs.FullDofCount()), + "The DofManager full dimension must match the full load.")); + } + const Status dof_status = dofs.ValidateInvariants(); + if (!dof_status.IsOk()) { + return Result::Failure(dof_status); + } + if (kfc.Rows() != dofs.FreeDofCount() || + kfc.Columns() != dofs.ConstrainedDofCount() || + prescribed_values.Size() != dofs.ConstrainedDofCount()) { + return Result::Failure(LoadFailure( + "invalid-load-dimensions", location, "LOAD_ASSEMBLER", + std::to_string(kfc.Rows()) + "x" + std::to_string(kfc.Columns()), + "Kfc rows/columns and prescribed values must match free/constrained " + "order.")); + } + const Status matrix_status = kfc.Validate(); + if (!matrix_status.IsOk()) { + return Result::Failure(matrix_status); + } + const Status load_status = + ValidateFiniteVector(full_load, location, "full-load"); + if (!load_status.IsOk()) { + return Result::Failure(load_status); + } + const Status prescribed_status = + ValidateFiniteVector(prescribed_values, location, "prescribed-values"); + if (!prescribed_status.IsOk()) { + return Result::Failure(prescribed_status); + } + + Vector entry_coefficients{kfc.Values().size()}; + Vector entry_prescribed{kfc.Values().size()}; + for (std::size_t position = 0U; position < kfc.Values().size(); ++position) { + entry_coefficients[position] = kfc.Values()[position]; + entry_prescribed[position] = + prescribed_values[kfc.ColumnIndices()[position]]; + } + Vector entry_products{0U}; + try { + entry_products = dense_blas_internal::MultiplyDiagonal(entry_coefficients, + entry_prescribed); + } catch (const std::length_error&) { + return Result::Failure(LoadFailure( + "load-backend-dimension-overflow", location, "LOAD_ASSEMBLER", "Kfc", + "Prescribed displacement products exceed the backend integer range.")); + } catch (const std::invalid_argument&) { + return Result::Failure(LoadFailure( + "invalid-load-product", location, "LOAD_ASSEMBLER", "Kfc", + "Prescribed displacement product inputs must be finite and compatible.")); + } + + // MKL computes independent entries; ordered CSR reduction below retains + // cancellation and the first product/row-sum overflow diagnostic. + Vector correction{kfc.Rows()}; + for (std::size_t row = 0U; row < kfc.Rows(); ++row) { + double sum = 0.0; + for (std::size_t position = kfc.RowOffsets()[row]; + position < kfc.RowOffsets()[row + 1U]; ++position) { + const double product = entry_products[position]; + if (!std::isfinite(product)) { + return Result::Failure(LoadFailure( + "nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", + std::to_string(row), + "Kfc times prescribed displacement produced a nonfinite product.")); + } + sum += product; + if (!std::isfinite(sum)) { + return Result::Failure(LoadFailure( + "nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", + std::to_string(row), + "Kfc times prescribed displacement produced a nonfinite row sum.")); + } + } + correction[row] = sum; + } + + Vector rhs = GatherFree(full_load, dofs); + // The constrained vector is already in DofManager order, so this is the + // approved elimination equation rhs = Ff - Kfc*dc without reordering dc. + for (std::size_t row = 0U; row < rhs.Size(); ++row) { + const double value = rhs[row] - correction[row]; + if (!std::isfinite(value)) { + return Result::Failure( + LoadFailure("nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", + std::to_string(row), + "Effective RHS subtraction produced a nonfinite value.")); + } + rhs[row] = value; + } + return Result::Success(std::move(rhs)); +} + } // namespace fesa diff --git a/src/fesa/core/ascii.cpp b/src/fesa/core/ascii.cpp index 7a6a24e..32466d3 100644 --- a/src/fesa/core/ascii.cpp +++ b/src/fesa/core/ascii.cpp @@ -13,6 +13,15 @@ char AsciiLower(const char value) noexcept { return value; } +std::string AsciiUppercase(std::string value) { + for (char& character : value) { + if (character >= 'a' && character <= 'z') { + character = static_cast(character - 'a' + 'A'); + } + } + return value; +} + bool AsciiCaseInsensitiveEquals(const std::string_view lhs, const std::string_view rhs) noexcept { if (lhs.size() != rhs.size()) { @@ -35,14 +44,13 @@ Result ParsePositiveSourceLabel(const std::string_view text) { if (parsed.ec == std::errc{} && parsed.ptr == last && value > 0) { return Result::Success(value); } - const std::string identity{text}; return Result::Failure(Status::Failure( FailureCategory::kInput, {{Severity::kError, "invalid-source-label", {{}, 0U}, "SOURCE_LABEL", - identity, + std::string{text}, "A source label must be a complete positive base-10 integer."}})); } diff --git a/src/fesa/elements/element_capabilities.cpp b/src/fesa/elements/element_capabilities.cpp new file mode 100644 index 0000000..772fc0c --- /dev/null +++ b/src/fesa/elements/element_capabilities.cpp @@ -0,0 +1,115 @@ +#include "fesa/elements/element_capabilities.h" + +#include + +#include "fesa/elements/hex8_element.h" +#include "fesa/elements/nonlinear_mitc4_inertia.h" +#include "fesa/elements/thermal_shell.h" + +namespace fesa { + +Mitc4TrialAdapter::Mitc4TrialAdapter(ElementDofLayout layout, + const NonlinearMitc4Shell& kernel) + : layout_{std::move(layout)}, kernel_{kernel} {} +const ElementDofLayout& Mitc4TrialAdapter::DofLayout() const noexcept { + return layout_; +} +Result Mitc4TrialAdapter::EvaluateTrial( + const NonlinearMitc4TrialDofs& trial, const Vector* direction) const { + return EvaluateInternalResidualAndTangent(kernel_, trial, direction); +} +Result +Mitc4TrialAdapter::EvaluateExternalDrillingConstraint( + std::size_t node, const Vector3& phi, const Vector3& phi_dot, + const Vector3& force, const Vector3& force_rate, + const Vector3& force_acceleration) const { + return kernel_.EvaluateExternalDrillingConstraint( + node, phi, phi_dot, force, force_rate, force_acceleration); +} + +Hex8TrialAdapter::Hex8TrialAdapter(const Hex8Element& kernel) + : kernel_{kernel} {} +const ElementDofLayout& Hex8TrialAdapter::DofLayout() const noexcept { + return kernel_.DofLayout(); +} +Result Hex8TrialAdapter::ComputeStiffness() const { + return kernel_.ComputeStiffness(); +} +Result Hex8TrialAdapter::Recover(const Vector& u) const { + return kernel_.Recover(u); +} +Result Hex8TrialAdapter::EvaluateLinear(const Vector& u) const { + return kernel_.EvaluateLinear(u); +} +Result Hex8TrialAdapter::EvaluateNonlinear(const Vector& u) const { + return kernel_.EvaluateNonlinear(u); +} +Result Hex8TrialAdapter::EvaluateLinearHighLow( + const Vector& high, const Vector& low) const { + return kernel_.EvaluateLinearHighLow(high, low); +} +Result Hex8TrialAdapter::EvaluateNonlinearHighLow( + const Vector& high, const Vector& low) const { + return kernel_.EvaluateNonlinearHighLow(high, low); +} +Result Hex8TrialAdapter::ComputeMass() const { + return kernel_.ComputeMass(); +} +std::array Hex8TrialAdapter::ReferenceWeights() const noexcept { + return kernel_.ReferenceWeights(); +} + +Mitc4InertiaAdapter::Mitc4InertiaAdapter( + ElementDofLayout layout, const NonlinearMitc4Inertia& kernel) + : layout_{std::move(layout)}, kernel_{kernel} {} +const ElementDofLayout& Mitc4InertiaAdapter::DofLayout() const noexcept { + return layout_; +} +Result Mitc4InertiaAdapter::Evaluate( + const NonlinearMitc4TrialDofs& trial, const Vector& velocity24, + const Vector& acceleration24) const { + return kernel_.Evaluate(trial, velocity24, acceleration24); +} + +Ds4ThermalAdapter::Ds4ThermalAdapter(const ThermalShell& kernel) + : layout_{kernel.SourceId(), kernel.NodeIndices()}, kernel_{kernel} {} +const ThermalElementDofLayout& Ds4ThermalAdapter::DofLayout() const noexcept { + return layout_; +} +const Matrix& Ds4ThermalAdapter::Conductivity() const noexcept { + return kernel_.Conductivity(); +} +const Matrix& Ds4ThermalAdapter::Capacity() const noexcept { + return kernel_.Capacity(); +} +const Matrix& Ds4ThermalAdapter::ConductivityError() const noexcept { + return kernel_.ConductivityError(); +} +const Matrix& Ds4ThermalAdapter::CapacityError() const noexcept { + return kernel_.CapacityError(); +} +Result Ds4ThermalAdapter::Recover( + const std::array& temperature) const { + return kernel_.Recover(temperature); +} + +Mitc4ReferenceAdapter::Mitc4ReferenceAdapter(const Mitc4Shell& kernel) + : kernel_{kernel} {} +const ElementDofLayout& Mitc4ReferenceAdapter::DofLayout() const noexcept { + return kernel_.DofLayout(); +} +Result +Mitc4ReferenceAdapter::ComputeStiffness() const { + return kernel_.ComputeStiffness(); +} +Result Mitc4ReferenceAdapter::Recover(const Vector& u) const { + return kernel_.Recover(u); +} +Result Mitc4ReferenceAdapter::Stiffness() const { + return kernel_.Stiffness(); +} +Result Mitc4ReferenceAdapter::Mass(double density) const { + return kernel_.Mass(density); +} + +} // namespace fesa diff --git a/src/fesa/elements/element_factory.cpp b/src/fesa/elements/element_factory.cpp index 5fffbfa..5f785a5 100644 --- a/src/fesa/elements/element_factory.cpp +++ b/src/fesa/elements/element_factory.cpp @@ -292,6 +292,18 @@ Result> ElementFactory::CreateSolid( return Result>::Success(std::move(result)); } +std::unique_ptr ElementFactory::BindSolidTrial( + const Element& kernel) const { + const auto* solid = dynamic_cast(&kernel); + return solid == nullptr ? nullptr : std::make_unique(*solid); +} + +std::unique_ptr ElementFactory::BindShellReference( + const Element& kernel) const { + const auto* shell = dynamic_cast(&kernel); + return shell == nullptr ? nullptr : std::make_unique(*shell); +} + Result> ElementFactory::Create( const ElementDefinition& definition, const Domain& domain) const { switch (definition.Kind()) { diff --git a/src/fesa/elements/finite_rotation.cpp b/src/fesa/elements/finite_rotation.cpp index 4e82390..8048f4d 100644 --- a/src/fesa/elements/finite_rotation.cpp +++ b/src/fesa/elements/finite_rotation.cpp @@ -6,25 +6,21 @@ #include #include +#include "math/dense_blas_internal.h" +#include "math/finite_values.h" +#include "math/vector3_operations.h" + namespace fesa { namespace { +using math_internal::Multiply; +using math_internal::Skew; +using math_internal::IsFinite; + constexpr std::size_t kRotationDimension = 3U; constexpr double kRotationStateTolerance = 1.0e-12; constexpr double kSmallAngleThreshold = 1.0e-4; -/// @brief Returns the SO(3) skew-symmetric matrix for a global vector. -Matrix Skew(const Vector3& vector) { - Matrix result{kRotationDimension, kRotationDimension}; - result(0U, 1U) = -vector.Z(); - result(0U, 2U) = vector.Y(); - result(1U, 0U) = vector.Z(); - result(1U, 2U) = -vector.X(); - result(2U, 0U) = -vector.Y(); - result(2U, 1U) = vector.X(); - return result; -} - /// @brief Returns the 3-by-3 identity matrix. Matrix Identity() { Matrix result{kRotationDimension, kRotationDimension}; @@ -63,18 +59,6 @@ double Determinant(const Matrix& matrix) { matrix(1U, 1U) * matrix(2U, 0U)); } -/// @brief Checks that all matrix entries are finite. -bool IsFinite(const Matrix& matrix) { - for (std::size_t row = 0U; row < matrix.Rows(); ++row) { - for (std::size_t column = 0U; column < matrix.Columns(); ++column) { - if (!std::isfinite(matrix(row, column))) { - return false; - } - } - } - return true; -} - /// @brief Checks the exact 3-by-3 proper-orthogonal frame contract. bool IsProperOrthogonal(const Matrix& matrix) { if (matrix.Rows() != kRotationDimension || @@ -113,17 +97,6 @@ bool IsPrincipalCoordinate(const Vector3& phi) { return std::isfinite(angle) && pi - angle > margin; } -/// @brief Multiplies a 3-by-3 frame by a global vector. -Vector3 Multiply(const Matrix& matrix, const Vector3& vector) { - return Vector3{ - matrix(0U, 0U) * vector.X() + matrix(0U, 1U) * vector.Y() + - matrix(0U, 2U) * vector.Z(), - matrix(1U, 0U) * vector.X() + matrix(1U, 1U) * vector.Y() + - matrix(1U, 2U) * vector.Z(), - matrix(2U, 0U) * vector.X() + matrix(2U, 1U) * vector.Y() + - matrix(2U, 2U) * vector.Z()}; -} - /// @brief Checks componentwise agreement without scale-dependent repair. bool IsNear(const Matrix& actual, const Matrix& expected) { if (actual.Rows() != expected.Rows() || @@ -225,25 +198,15 @@ Matrix LeftJacobianSO3(const Vector3& phi) { quadratic_coefficient); } -Matrix InverseTransposeLeftJacobianSO3(const Vector3& phi) { - const Matrix skew = Skew(phi); - const Matrix skew_squared = skew.Multiply(skew); - const double theta = phi.Norm(); - const double theta_squared = theta * theta; - - double quadratic_coefficient = 1.0 / 12.0; - if (theta >= kSmallAngleThreshold) { - const double half_theta = 0.5 * theta; - quadratic_coefficient = - (1.0 - half_theta / std::tan(half_theta)) / theta_squared; - } else { - const double theta_fourth = theta_squared * theta_squared; - const double theta_sixth = theta_fourth * theta_squared; - quadratic_coefficient = - 1.0 / 12.0 + theta_squared / 720.0 + theta_fourth / 30240.0 + - theta_sixth / 1209600.0; - } - return Polynomial(skew, skew_squared, 0.5, quadratic_coefficient); +Result SpatialMomentSO3(const Vector3& phi, const Vector3& generalized) { + const auto factor = dense_blas_internal::DenseFactor::Lu(LeftJacobianSO3(phi)); + if (!factor.HasValue()) return Result::Failure(factor.GetStatus()); + Matrix rhs{3U, 1U}; + for (std::size_t i = 0; i < 3U; ++i) rhs(i, 0U) = generalized[i]; + const auto solved = factor.Value().Solve(rhs, true); + if (!solved.HasValue()) return Result::Failure(solved.GetStatus()); + return Result::Success( + Vector3{solved.Value()(0, 0), solved.Value()(1, 0), solved.Value()(2, 0)}); } Result MakeFiniteRotationState( diff --git a/src/fesa/elements/hex8_element.cpp b/src/fesa/elements/hex8_element.cpp index 71de886..bb25f4b 100644 --- a/src/fesa/elements/hex8_element.cpp +++ b/src/fesa/elements/hex8_element.cpp @@ -1,5 +1,7 @@ #include "fesa/elements/hex8_element.h" +#include "fesa/math/vector3.h" + #include #include #include @@ -180,17 +182,46 @@ Result Hex8Element::Create(EntityIndex index, point.weight <= 0.0) return Result::Failure(candidate.Failure( "Reference quadrature Jacobian must be finite and positive.")); - const auto inverse = hex8_internal::Inverse(jacobian, point.weight); - if (!Finite(inverse)) + const Vector3 c0{jacobian[0][0], jacobian[1][0], jacobian[2][0]}; + const Vector3 c1{jacobian[0][1], jacobian[1][1], jacobian[2][1]}; + const Vector3 c2{jacobian[0][2], jacobian[1][2], jacobian[2][2]}; + if (!c0.Cross(c1).IsFinite() || !c1.Cross(c2).IsFinite() || + !c2.Cross(c0).IsFinite()) return Result::Failure( candidate.Failure("Reference Jacobian inverse is nonfinite.")); + Matrix coefficients{3U, 3U}; + Matrix natural_gradients{3U, 8U}; + for (std::size_t i = 0; i < 3U; ++i) { + for (std::size_t d = 0; d < 3U; ++d) + coefficients(i, d) = jacobian[i][d]; + for (std::size_t a = 0; a < 8U; ++a) + natural_gradients(i, a) = derivatives[a][i]; + } + const auto factor = dense_blas_internal::DenseFactor::Lu(coefficients); + if (!factor.HasValue()) + return Result::Failure( + candidate.Failure("Reference Jacobian inverse is nonfinite.")); + // The original geometry gate required every reciprocal action to be + // finite, including entries not exposed by the smaller shape-gradient RHS. + Matrix canonical{3U, 1U}; + for (std::size_t axis = 0; axis < 3U; ++axis) { + if (axis != 0U) canonical(axis - 1U, 0U) = 0.0; + canonical(axis, 0U) = 1.0; + if (!factor.Value().Solve(canonical, true).HasValue()) + return Result::Failure( + candidate.Failure("Reference Jacobian inverse is nonfinite.")); + } + // J has Cartesian rows and natural columns: J^T grad_x N = grad_nat N. + const auto gradients = factor.Value().Solve(natural_gradients, true); + if (!gradients.HasValue()) + return Result::Failure(candidate.Failure( + "Reference gradient or point coordinate is nonfinite.")); point.position = coordinates[0]; for (std::size_t a = 0; a < 8; ++a) for (std::size_t i = 0; i < 3; ++i) { point.position[i] += point.shape[a] * (coordinates[a][i] - coordinates[0][i]); - for (std::size_t d = 0; d < 3; ++d) - point.gradients[a][i] += inverse[d][i] * derivatives[a][d]; + point.gradients[a][i] = gradients.Value()(i, a); if (!std::isfinite(point.gradients[a][i]) || !std::isfinite(point.position[i])) return Result::Failure(candidate.Failure( diff --git a/src/fesa/elements/hex8_geometry.cpp b/src/fesa/elements/hex8_geometry.cpp index e7eff40..6d444cf 100644 --- a/src/fesa/elements/hex8_geometry.cpp +++ b/src/fesa/elements/hex8_geometry.cpp @@ -228,17 +228,4 @@ double Determinant(const Tensor& j) { j[0][1] * (j[1][0] * j[2][2] - j[1][2] * j[2][0]) + j[0][2] * (j[1][0] * j[2][1] - j[1][1] * j[2][0]); } -Tensor Inverse(const Tensor& j, double determinant) { - Tensor inverse{}; - for (std::size_t i = 0; i < 3; ++i) - for (std::size_t d = 0; d < 3; ++d) { - const auto r1 = (d + 1U) % 3U; - const auto r2 = (d + 2U) % 3U; - const auto c1 = (i + 1U) % 3U; - const auto c2 = (i + 2U) % 3U; - inverse[i][d] = - (j[r1][c1] * j[r2][c2] - j[r1][c2] * j[r2][c1]) / determinant; - } - return inverse; -} } // namespace fesa::hex8_internal diff --git a/src/fesa/elements/hex8_geometry.h b/src/fesa/elements/hex8_geometry.h index 6e670c7..72bf299 100644 --- a/src/fesa/elements/hex8_geometry.h +++ b/src/fesa/elements/hex8_geometry.h @@ -26,8 +26,6 @@ Tensor Jacobian(const Hex8Element::Coordinates& coordinates, const std::array& derivatives); /// @brief Computes the signed determinant; no absolute value or clamp. double Determinant(const Tensor& matrix); -/// @brief Computes the inverse for an already validated positive determinant. -Tensor Inverse(const Tensor& matrix, double determinant); } // namespace fesa::hex8_internal #endif // FESA_ELEMENTS_HEX8_GEOMETRY_H_ diff --git a/src/fesa/elements/hex8_high_low.cpp b/src/fesa/elements/hex8_high_low.cpp index ed1f9dc..90cfd2c 100644 --- a/src/fesa/elements/hex8_high_low.cpp +++ b/src/fesa/elements/hex8_high_low.cpp @@ -8,7 +8,6 @@ #include "../math/high_low_arithmetic.h" #include "hex8_geometry.h" #include "hex8_linear_tangent_cache.h" -#include "hex8_pair_arithmetic.h" #include "hex8_pair_simd.h" namespace fesa { @@ -18,13 +17,13 @@ using Triple = std::array; using Tensor = std::array; HighLow operator+(HighLow a, HighLow b) { - return hex8_internal::AddPair(a, b); + return math_internal::Add(a, b); } HighLow operator-(HighLow a, HighLow b) { return math_internal::Subtract(a, b); } HighLow operator*(HighLow a, HighLow b) { - return hex8_internal::MultiplyPair(a, b); + return math_internal::Multiply(a, b); } HighLow operator/(HighLow a, HighLow b) { return math_internal::Divide(a, b); diff --git a/src/fesa/elements/hex8_pair_arithmetic.h b/src/fesa/elements/hex8_pair_arithmetic.h deleted file mode 100644 index d2fdd5f..0000000 --- a/src/fesa/elements/hex8_pair_arithmetic.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef FESA_SRC_FESA_ELEMENTS_HEX8_PAIR_ARITHMETIC_H_ -#define FESA_SRC_FESA_ELEMENTS_HEX8_PAIR_ARITHMETIC_H_ - -#include "../math/high_low_arithmetic.h" - -namespace fesa::hex8_internal { -/// @brief Preserves original zero-add normalization without redundant TwoSums. -/// @note Valid nonzero terms retain the original addition association. -inline math_internal::HighLow AddPair(math_internal::HighLow left, - math_internal::HighLow right) { - using namespace math_internal; - if (!IsNormalized(left) || !IsNormalized(right)) return InvalidHighLow(); - if (left.high == 0 && left.low == 0) - return {right.high, right.low == 0 ? 0.0 : right.low}; - if (right.high == 0 && right.low == 0) - return {left.high, left.low == 0 ? 0.0 : left.low}; - return Add(left, right); -} -/// @brief Omits exact-zero cross products without changing nonzero association. -/// @note A retained zero addition performs the original canonicalization and -/// invalid-result check; its repeated application is idempotent. -inline math_internal::HighLow MultiplyPair(math_internal::HighLow left, - math_internal::HighLow right) { - using namespace math_internal; - if (!IsNormalized(left) || !IsNormalized(right)) return InvalidHighLow(); - if (left.low != 0 && right.low != 0) return Multiply(left, right); - auto product = TwoProduct(left.high, right.high); - if (right.low != 0) { - product = AddPair(product, TwoProduct(left.high, right.low)); - } else if (left.low != 0) { - // The original zero addition precedes the nonzero low-high term. - product = AddPair(product, HighLow{}); - } - if (left.low != 0) - product = AddPair(product, TwoProduct(left.low, right.high)); - return AddPair(product, HighLow{}); -} -} // namespace fesa::hex8_internal - -#endif // FESA_SRC_FESA_ELEMENTS_HEX8_PAIR_ARITHMETIC_H_ diff --git a/src/fesa/elements/hex8_pair_simd.h b/src/fesa/elements/hex8_pair_simd.h index 4dffa07..4e5a933 100644 --- a/src/fesa/elements/hex8_pair_simd.h +++ b/src/fesa/elements/hex8_pair_simd.h @@ -60,11 +60,11 @@ void EvaluateMaterialPacketScalar(const MaterialPacketInput& input, void EvaluateMaterialPacketAvx2(const MaterialPacketInput& input, PairPacket* output); /// @brief Baseline-ISA checked addition bridge in lane order 0..3. -/// @note pair_shortcuts chooses the original local AddPair or general Add graph; +/// @note Uses the shared checked high/low arithmetic owner; /// invalid pairs are preserved, with no FP-state mutation or normalization here. void ScalarPacketAdd(const PairPacket& left, const PairPacket& right, - bool pair_shortcuts, PairPacket* output); -/// @brief Baseline-ISA original checked MultiplyPair bridge in lane order 0..3. + PairPacket* output); +/// @brief Baseline-ISA shared checked multiplication bridge in lane order 0..3. /// @note Raw invalid outputs remain invalid; no component is rounded away. void ScalarPacketMultiply(const PairPacket& left, const PairPacket& right, PairPacket* output); diff --git a/src/fesa/elements/hex8_pair_simd_avx2.cpp b/src/fesa/elements/hex8_pair_simd_avx2.cpp index 570b2a5..0a6e597 100644 --- a/src/fesa/elements/hex8_pair_simd_avx2.cpp +++ b/src/fesa/elements/hex8_pair_simd_avx2.cpp @@ -144,11 +144,11 @@ bool TwoSum(__m256d left, __m256d right, VectorPair* output) { return true; } /// @brief Routes exceptional packets to baseline checked Add in lane order. -VectorPair ScalarAdd(VectorPair left, VectorPair right, bool pair_shortcuts) { +VectorPair ScalarAdd(VectorPair left, VectorPair right) { const auto a = Store(left); const auto b = Store(right); PairPacket result; - ScalarPacketAdd(a, b, pair_shortcuts, &result); + ScalarPacketAdd(a, b, &result); return Load(result); } /// @brief Routes exceptional packets to baseline checked MultiplyPair. @@ -160,7 +160,7 @@ VectorPair ScalarMultiply(const PairPacket& left, const PairPacket& right) { /// @brief Retains normalized/zero branches and the checked per-lane Add graph. VectorPair Add(VectorPair left, VectorPair right, bool pair_shortcuts) { if (!NormalizedInputs(left, right)) - return ScalarAdd(left, right, pair_shortcuts); + return ScalarAdd(left, right); const int left_zero = _mm256_movemask_pd( _mm256_and_pd(Zero(left.high), Zero(left.low))); const int right_zero = _mm256_movemask_pd( @@ -171,7 +171,7 @@ VectorPair Add(VectorPair left, VectorPair right, bool pair_shortcuts) { if (right_zero == 15) return {left.high, Select(Zero(left.low), _mm256_setzero_pd(), left.low)}; if (left_zero != 0 || right_zero != 0) - return ScalarAdd(left, right, true); + return ScalarAdd(left, right); } VectorPair head; VectorPair tail; @@ -181,7 +181,7 @@ VectorPair Add(VectorPair left, VectorPair right, bool pair_shortcuts) { !TwoSum(left.low, right.low, &tail) || !TwoSum(head.low, tail.high, &middle) || !TwoSum(head.high, middle.high, &joined)) - return ScalarAdd(left, right, pair_shortcuts); + return ScalarAdd(left, right); const auto remainder = _mm256_add_pd( _mm256_add_pd(joined.low, middle.low), tail.low); const auto valid = _mm256_and_pd(Finite(remainder), _mm256_and_pd( @@ -193,7 +193,7 @@ VectorPair Add(VectorPair left, VectorPair right, bool pair_shortcuts) { VectorPair result; if (!TwoSum(Select(valid, joined.high, _mm256_setzero_pd()), Select(valid, remainder, _mm256_setzero_pd()), &result)) - return ScalarAdd(left, right, pair_shortcuts); + return ScalarAdd(left, right); return InvalidOutside(result, valid); } /// @brief Checks finite/zero/tail premises before products; neutralizes rejected diff --git a/src/fesa/elements/hex8_pair_simd_dispatch.cpp b/src/fesa/elements/hex8_pair_simd_dispatch.cpp index 97e0118..9c97047 100644 --- a/src/fesa/elements/hex8_pair_simd_dispatch.cpp +++ b/src/fesa/elements/hex8_pair_simd_dispatch.cpp @@ -1,6 +1,6 @@ #include "hex8_pair_simd.h" -#include "hex8_pair_arithmetic.h" +#include "../math/high_low_arithmetic.h" #if defined(FESA_HEX8_PAIR_AVX2) && defined(_MSC_VER) && defined(_M_X64) #include @@ -16,11 +16,11 @@ namespace fesa::hex8_internal { #endif FESA_HEX8_BASELINE_NOINLINE void ScalarPacketAdd(const PairPacket& left, const PairPacket& right, - bool pair_shortcuts, PairPacket* output) { + PairPacket* output) { for (std::size_t lane = 0; lane < 4; ++lane) { const math_internal::HighLow a{left.values[lane].high, left.values[lane].low}; const math_internal::HighLow b{right.values[lane].high, right.values[lane].low}; - const auto value = pair_shortcuts ? AddPair(a, b) : math_internal::Add(a, b); + const auto value = math_internal::Add(a, b); output->values[lane] = {value.high, value.low}; } } @@ -28,7 +28,7 @@ FESA_HEX8_BASELINE_NOINLINE void ScalarPacketMultiply(const PairPacket& left, const PairPacket& right, PairPacket* output) { for (std::size_t lane = 0; lane < 4; ++lane) { - const auto value = MultiplyPair( + const auto value = math_internal::Multiply( {left.values[lane].high, left.values[lane].low}, {right.values[lane].high, right.values[lane].low}); output->values[lane] = {value.high, value.low}; @@ -51,14 +51,14 @@ void EvaluateGeometryPacketScalar(const GeometryPacketInput& input, for (std::size_t lane = 0; lane < 4; ++lane) { math_internal::HighLow dot; for (std::size_t i = 0; i < 3; ++i) - dot = AddPair(dot, MultiplyPair(value(input.gradient_a[i]), + dot = math_internal::Add(dot, math_internal::Multiply(value(input.gradient_a[i]), value(input.gradient_b[i].values[lane]))); math_internal::HighLow geo; for (std::size_t j = 0; j < 3; ++j) for (std::size_t l = 0; l < 3; ++l) - geo = AddPair(geo, MultiplyPair(value(input.gradient_stress[j][l]), + geo = math_internal::Add(geo, math_internal::Multiply(value(input.gradient_stress[j][l]), value(input.gradient_b[l].values[lane]))); - const auto weighted = MultiplyPair(geo, value(input.weight)); + const auto weighted = math_internal::Multiply(geo, value(input.weight)); output->gradient_dot.values[lane] = {dot.high, dot.low}; output->weighted_geometric.values[lane] = {weighted.high, weighted.low}; } @@ -84,15 +84,15 @@ void EvaluateMaterialPacketScalar(const MaterialPacketInput& input, return math_internal::HighLow{pair.high, pair.low}; }; for (std::size_t lane = 0; lane < 4; ++lane) { - const auto first = MultiplyPair(value(input.deviatoric_left.values[lane]), + const auto first = math_internal::Multiply(value(input.deviatoric_left.values[lane]), value(input.deviatoric_right.values[lane])); - const auto second = MultiplyPair(value(input.shear_left.values[lane]), + const auto second = math_internal::Multiply(value(input.shear_left.values[lane]), value(input.shear_right.values[lane])); - const auto third = MultiplyPair(value(input.tensor_left.values[lane]), + const auto third = math_internal::Multiply(value(input.tensor_left.values[lane]), value(input.gradient_dot.values[lane])); - const auto material = AddPair(AddPair(first, second), third); - const auto accumulated = AddPair(value(input.accumulated.values[lane]), - MultiplyPair(material, value(input.weight))); + const auto material = math_internal::Add(math_internal::Add(first, second), third); + const auto accumulated = math_internal::Add(value(input.accumulated.values[lane]), + math_internal::Multiply(material, value(input.weight))); output->values[lane] = {accumulated.high, accumulated.low}; } } diff --git a/src/fesa/elements/mitc4_shell.cpp b/src/fesa/elements/mitc4_shell.cpp index 129f05b..6aea36e 100644 --- a/src/fesa/elements/mitc4_shell.cpp +++ b/src/fesa/elements/mitc4_shell.cpp @@ -9,6 +9,9 @@ #include "fesa/model/model_types.h" #include "math/dense_blas_internal.h" +#include "math/finite_values.h" +#include "math/vector3_operations.h" +#include "elements/shell_kernel_values.h" namespace fesa { @@ -63,6 +66,13 @@ void Mitc4ShellDefinition::SynchronizeNodeIndices() { namespace { +using shell_kernel_internal::FrameComponent; +using shell_kernel_internal::LocalEngineeringComponents; +using shell_kernel_internal::ElementIdentity; +using math_internal::DerivativeSum; +using math_internal::WeightedSum; +using math_internal::IsFinite; + constexpr std::size_t kNodeCount = 4U; constexpr std::size_t kPhysicalDofsPerNode = 5U; constexpr std::size_t kGlobalDofsPerNode = 6U; @@ -71,24 +81,6 @@ constexpr std::size_t kGlobalDofCount = 24U; constexpr double kShearCorrection = 5.0 / 6.0; constexpr double kFrameTolerance = 1.0e-12; -Vector3 WeightedSum(const std::array& weights, - const std::array& values) { - Vector3 result{}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - result = result + weights[node] * values[node]; - } - return result; -} - -Vector3 DerivativeSum(const std::array& derivatives, - const std::array& values) { - std::array relative{}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - relative[node] = values[node] - values[0]; - } - return WeightedSum(derivatives, relative); -} - bool SameCoordinates(const Vector3& left, const Vector3& right) { return left == right; } @@ -129,20 +121,6 @@ std::array NodalTangentsB( return tangents; } -std::string ElementIdentity(const std::array& nodes) { - std::string identity; - for (const Node* node : nodes) { - if (node == nullptr) { - continue; - } - if (!identity.empty()) { - identity += "-"; - } - identity += node->source_id.source_label_text; - } - return identity; -} - Result ModelFailure(std::string code, const SourceLocation& location, const std::string& identity, @@ -169,58 +147,36 @@ std::array, 3> CovariantStrainColumn( return strain; } -/// @brief Reconstructs Cartesian strain in the approved reciprocal-basis -/// reduction order. +/// @brief Solves C^T E C = E_cov for the actual strain tensor. std::array, 3> ReconstructCartesianStrain( const std::array, 3>& covariant_strain, - const std::array& reciprocal) { + const dense_blas_internal::DenseFactor& factor) { + Matrix rhs{3U, 3U}; + for (std::size_t i = 0; i < 3U; ++i) + for (std::size_t j = 0; j < 3U; ++j) rhs(i, j) = covariant_strain[i][j]; + const auto left = factor.Solve(rhs, true); + if (!left.HasValue()) + throw std::invalid_argument{"MITC4 Cartesian strain is nonfinite."}; + const auto solved = factor.SolveRight(left.Value()); + if (!solved.HasValue()) + throw std::invalid_argument{"MITC4 Cartesian strain is nonfinite."}; std::array, 3> tensor{}; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - for (std::size_t first = 0U; first < 3U; ++first) { - for (std::size_t second = 0U; second < 3U; ++second) { - tensor[row][column] += covariant_strain[first][second] * - reciprocal[first][row] * - reciprocal[second][column]; - } - } - } - } + for (std::size_t i = 0; i < 3U; ++i) + for (std::size_t j = 0; j < 3U; ++j) tensor[i][j] = solved.Value()(i, j); return tensor; } -double FrameComponent(const Vector3& left, - const std::array, 3>& tensor, - const Vector3& right) { - double value = 0.0; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - value += left[row] * tensor[row][column] * right[column]; - } - } - return value; -} - -/// @brief Projects tensor strain into signed local engineering components. -std::array LocalEngineeringComponents( - const std::array, 3>& tensor, - const Mitc4LocalFrame& frame) { - const Vector3 e1{frame.e1}; - const Vector3 e2{frame.e2}; - const Vector3 e3{frame.e3}; - return {FrameComponent(e1, tensor, e1), FrameComponent(e2, tensor, e2), - 2.0 * FrameComponent(e1, tensor, e2), - 2.0 * FrameComponent(e1, tensor, e3), - 2.0 * FrameComponent(e2, tensor, e3)}; +/// @brief Packs covariant columns without constructing reciprocal vectors. +Matrix CovariantMatrix(const std::array& covariant) { + Matrix coefficients{3U, 3U}; + for (std::size_t i = 0; i < 3U; ++i) + for (std::size_t j = 0; j < 3U; ++j) coefficients(i, j) = covariant[j][i]; + return coefficients; } Matrix ScaledMatrix(const Matrix& source, double factor) { - Matrix result{source.Rows(), source.Columns()}; - for (std::size_t row = 0U; row < source.Rows(); ++row) { - for (std::size_t column = 0U; column < source.Columns(); ++column) { - result(row, column) = factor * source(row, column); - } - } + Matrix result{source}; + result.Scale(factor); return result; } @@ -241,26 +197,6 @@ Matrix Congruence(const Matrix& local, const Matrix& transformation) { return result; } -bool IsFinite(const Matrix& matrix) { - for (std::size_t row = 0U; row < matrix.Rows(); ++row) { - for (std::size_t column = 0U; column < matrix.Columns(); ++column) { - if (!std::isfinite(matrix(row, column))) { - return false; - } - } - } - return true; -} - -bool IsFinite(const Vector& vector) { - for (std::size_t index = 0U; index < vector.Size(); ++index) { - if (!std::isfinite(vector[index])) { - return false; - } - } - return true; -} - Result StiffnessFailure(const SourceLocation& location, const std::string& identity, std::string message) { @@ -1092,12 +1028,25 @@ bool Mitc4Shell::EvaluateGeometry(double xi, double eta, double zeta, return false; } - result.reciprocal[0] = - (1.0 / result.jacobian) * result.covariant[1].Cross(result.covariant[2]); - result.reciprocal[1] = - (1.0 / result.jacobian) * result.covariant[2].Cross(result.covariant[0]); - result.reciprocal[2] = - (1.0 / result.jacobian) * result.covariant[0].Cross(result.covariant[1]); + if (!std::isfinite(1.0 / result.jacobian) || + !result.covariant[1].Cross(result.covariant[2]).IsFinite() || + !result.covariant[2].Cross(result.covariant[0]).IsFinite() || + !result.covariant[0].Cross(result.covariant[1]).IsFinite()) return false; + // Preserve the reciprocal finiteness gate, streaming each action directly + // into validation rather than retaining a reciprocal-basis matrix. + try { + const auto factor = dense_blas_internal::DenseFactor::Lu( + CovariantMatrix(result.covariant)); + if (!factor.HasValue()) return false; + Matrix rhs{3U, 1U}; + for (std::size_t axis = 0; axis < 3U; ++axis) { + if (axis != 0U) rhs(axis - 1U, 0U) = 0.0; + rhs(axis, 0U) = 1.0; + if (!factor.Value().Solve(rhs, true).HasValue()) return false; + } + } catch (const std::exception&) { + return false; + } const Vector3 area = midsurface_xi.Cross(midsurface_eta); const double director_norm = director_value.Norm(); @@ -1120,8 +1069,7 @@ bool Mitc4Shell::EvaluateGeometry(double xi, double eta, double zeta, const Vector3 e2 = e3.Cross(e1); result.frame.e1 = e1.Components(); result.frame.e2 = e2.Components(); - return result.reciprocal[0].IsFinite() && result.reciprocal[1].IsFinite() && - result.reciprocal[2].IsFinite() && e2.IsFinite(); + return e2.IsFinite(); } std::array, 20> Mitc4Shell::BasisDerivatives( @@ -1165,6 +1113,11 @@ Matrix Mitc4Shell::StrainDisplacement(double xi, double eta, double zeta, throw std::invalid_argument{ "MITC4 strain geometry is invalid at the requested point."}; } + const auto factor = dense_blas_internal::DenseFactor::Lu( + CovariantMatrix(geometry.covariant)); + if (!factor.HasValue()) + throw std::invalid_argument{ + "MITC4 strain geometry is invalid at the requested point."}; const auto derivatives = BasisDerivatives(xi, eta, zeta); const Mitc4TyingWeights weights = TyingWeights(xi, eta); Matrix result{5U, kPhysicalDofCount}; @@ -1180,7 +1133,7 @@ Matrix Mitc4Shell::StrainDisplacement(double xi, double eta, double zeta, covariant[2U][1U] = covariant[1U][2U]; } const auto engineering = LocalEngineeringComponents( - ReconstructCartesianStrain(covariant, geometry.reciprocal), + ReconstructCartesianStrain(covariant, factor.Value()), geometry.frame); for (std::size_t component = 0U; component < engineering.size(); ++component) { diff --git a/src/fesa/elements/nonlinear_mitc4_inertia.cpp b/src/fesa/elements/nonlinear_mitc4_inertia.cpp index 13198fb..b0716d4 100644 --- a/src/fesa/elements/nonlinear_mitc4_inertia.cpp +++ b/src/fesa/elements/nonlinear_mitc4_inertia.cpp @@ -11,32 +11,29 @@ #include "fesa/elements/finite_rotation.h" #include "math/dense_blas_internal.h" +#include "math/jet_arithmetic.h" +#include "math/jet_linear_solve.h" +#include "math/finite_values.h" +#include "math/vector3_operations.h" +#include "elements/shell_kernel_values.h" +#include "math/compensated_sum.h" namespace fesa { namespace { +using shell_kernel_internal::Failure; +using InertiaSum = math_internal::CompensatedSum; +using shell_kernel_internal::FrameMatrix; +using math_internal::DerivativeSum; +using math_internal::WeightedSum; +using math_internal::IsFinite; + constexpr std::size_t kNodes = 4U; constexpr std::size_t kDofs = 24U; const std::array kGauss{-std::sqrt(3.0 / 5.0), 0.0, std::sqrt(3.0 / 5.0)}; constexpr std::array kWeights{5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0}; -class InertiaSum { - public: - void Add(const double term) { - const double next = sum_ + term; - correction_ += std::abs(sum_) >= std::abs(term) - ? (sum_ - next) + term - : (term - next) + sum_; - sum_ = next; - } - double Value() const { return sum_ + correction_; } - - private: - double sum_ = 0.0; - double correction_ = 0.0; -}; - /// @brief Contracts two nodal 6-by-3 coefficient blocks through dense MKL. /// @note Nonfinite coefficients remain failures of the final inertia gate. void DotBlocks(const std::array& left, @@ -47,25 +44,6 @@ void DotBlocks(const std::array& left, result.fill(std::numeric_limits::quiet_NaN()); } -Vector3 WeightedSum(const std::array& weights, - const std::array& values) { - Vector3 result{}; - for (std::size_t node = 0U; node < kNodes; ++node) - result = result + weights[node] * values[node]; - return result; -} - -// A derivative partition has no absolute-position contribution. Anchoring -// before reduction preserves that identity at large global offsets and keeps -// the reference/current covariants in the same canonical form. -Vector3 DerivativeSum(const std::array& derivatives, - const std::array& values) { - std::array relative{}; - for (std::size_t node = 0U; node < kNodes; ++node) - relative[node] = values[node] - values[0U]; - return WeightedSum(derivatives, relative); -} - Vector3 InterpolatedSum(const std::array& weights, const std::array& values) { std::array relative{}; @@ -74,14 +52,6 @@ Vector3 InterpolatedSum(const std::array& weights, return values[0U] + WeightedSum(weights, relative); } -template -Result Failure(const std::string& code, const SourceLocation& location, - const std::string& identity, std::string message) { - return Result::Failure(Status::Failure( - FailureCategory::kModel, {{Severity::kError, code, location, "*ELEMENT", - identity, std::move(message)}})); -} - std::string Identity(const std::array& nodes) { std::string identity; for (const Node* node : nodes) { @@ -93,36 +63,6 @@ std::string Identity(const std::array& nodes) { return identity; } -Matrix FrameMatrix(const ShellNodeInitialFrame& frame) { - Matrix result{3U, 3U}; - for (std::size_t row = 0U; row < 3U; ++row) { - result(row, 0U) = frame.tangent_a[row]; - result(row, 1U) = frame.tangent_b[row]; - result(row, 2U) = frame.director[row]; - } - return result; -} - -bool IsFinite(const Matrix& matrix) { - for (std::size_t row = 0U; row < matrix.Rows(); ++row) { - for (std::size_t column = 0U; column < matrix.Columns(); ++column) { - if (!std::isfinite(matrix(row, column))) { - return false; - } - } - } - return true; -} - -bool IsFinite(const Vector& vector) { - for (std::size_t index = 0U; index < vector.Size(); ++index) { - if (!std::isfinite(vector[index])) { - return false; - } - } - return true; -} - /// @brief Carries all derivatives through order three in one nodal rotation. struct Jet { double value{}; @@ -189,31 +129,6 @@ Jet operator*(const Jet& left, const Jet& right) { return result; } -Jet Reciprocal(const Jet& value) { - Jet result{}; - result.value = 1.0 / value.value; - const double squared = result.value * result.value; - const double cubed = squared * result.value; - const double fourth = cubed * result.value; - for (std::size_t i = 0U; i < 3U; ++i) { - result.d[i] = -value.d[i] * squared; - for (std::size_t j = 0U; j < 3U; ++j) { - result.dd[i][j] = - 2.0 * value.d[i] * value.d[j] * cubed - value.dd[i][j] * squared; - for (std::size_t k = 0U; k < 3U; ++k) { - result.ddd[i][j][k] = - -6.0 * value.d[i] * value.d[j] * value.d[k] * fourth + - 2.0 * - (value.dd[i][j] * value.d[k] + value.dd[i][k] * value.d[j] + - value.dd[j][k] * value.d[i]) * - cubed - - value.ddd[i][j][k] * squared; - } - } - } - return result; -} - Jet InverseSqrt(const Jet& value) { Jet result{}; result.value = 1.0 / std::sqrt(value.value); @@ -246,27 +161,11 @@ using JetVector = std::array; using JetMatrix = std::array, 3>; JetVector Multiply(const JetMatrix& matrix, const JetVector& vector) { - JetVector result{Constant(0.0), Constant(0.0), Constant(0.0)}; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - result[row] = result[row] + matrix[row][column] * vector[column]; - } - } - return result; + return math_internal::MultiplyJetMatrixVector(matrix, vector, Constant); } JetMatrix Multiply(const JetMatrix& left, const JetMatrix& right) { - JetMatrix result{}; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - result[row][column] = Constant(0.0); - for (std::size_t index = 0U; index < 3U; ++index) { - result[row][column] = - result[row][column] + left[row][index] * right[index][column]; - } - } - } - return result; + return math_internal::MultiplyJetMatrix(left, right, Constant); } JetMatrix IdentityMatrix() { @@ -296,30 +195,32 @@ Jet Dot(const JetVector& left, const JetVector& right) { return result; } -JetMatrix Inverse(const JetMatrix& matrix) { - JetMatrix cofactor{}; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - const std::size_t a = (row + 1U) % 3U; - const std::size_t b = (row + 2U) % 3U; - const std::size_t c = (column + 1U) % 3U; - const std::size_t d = (column + 2U) % 3U; - cofactor[row][column] = - matrix[a][c] * matrix[b][d] - matrix[a][d] * matrix[b][c]; - // Cyclic minor ordering already includes the cofactor sign. - } - } - Jet determinant = Constant(0.0); - for (std::size_t column = 0U; column < 3U; ++column) { - determinant = determinant + matrix[0U][column] * cofactor[0U][column]; - } - const Jet inverse_determinant = Reciprocal(determinant); - JetMatrix result{}; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - result[row][column] = cofactor[column][row] * inverse_determinant; - } - } +/// @brief Reads every nodal-rotation derivative in increasing total order. +double ReadChannel(const Jet& jet, std::size_t degree, std::size_t channel) { + if (degree == 0) return jet.value; + if (degree == 1) return jet.d[channel]; + if (degree == 2) return jet.dd[channel / 3][channel % 3]; + return jet.ddd[channel / 9][(channel / 3) % 3][channel % 3]; +} +void WriteChannel(Jet& jet, std::size_t degree, std::size_t channel, + double value) { + if (degree == 0) jet.value = value; + else if (degree == 1) jet.d[channel] = value; + else if (degree == 2) jet.dd[channel / 3][channel % 3] = value; + else jet.ddd[channel / 9][(channel / 3) % 3][channel % 3] = value; +} +/// @brief Solves the actual null-preimage RHS with all third derivatives. +JetVector Solve(const JetMatrix& matrix, const JetVector& rhs) { + Matrix primal{3, 3}; + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t column = 0; column < 3; ++column) + primal(row, column) = matrix[row][column].value; + const auto factor = dense_blas_internal::DenseFactor::Lu(primal); + JetVector result{}; + if (factor.HasValue() && math_internal::SolveJetAction( + matrix, rhs, factor.Value(), {1, 3, 9, 27}, Constant, ReadChannel, + WriteChannel, result)) return result; + result.fill(Constant(std::numeric_limits::quiet_NaN())); return result; } @@ -486,27 +387,12 @@ Status CheckPhysicalMass(const Matrix& mass, const Matrix mass_complement = scaled.Multiply(complement); const Matrix block = dense_blas_internal::MultiplyTransposeLeft(complement, mass_complement); - double block_norm = 0.0; - for (std::size_t row = 0U; row < kPhysical; ++row) { - double row_sum = 0.0; - for (std::size_t column = 0U; column < kPhysical; ++column) { - row_sum += std::abs(block(row, column)); - } - block_norm = std::max(block_norm, row_sum); - } - Matrix lower{kPhysical, kPhysical}; - Matrix inverse_lower{kPhysical, kPhysical}; - auto factor_failure = dense_blas_internal::CholeskyFailure::kNone; - if (!dense_blas_internal::CholeskyLowerAndInverse(block, lower, inverse_lower, - &factor_failure)) { - if (factor_failure == dense_blas_internal::CholeskyFailure::kInverse) - return fail( - "ill-conditioned-nonlinear-inertia-positive-mass", - "Physical mass reciprocal infinity condition is below 2^-32."); + const auto factor = dense_blas_internal::DenseFactor::Cholesky(block); + if (!factor.HasValue()) { return fail("invalid-nonlinear-inertia-positive-mass", "Physical mass requires strictly positive Cholesky pivots."); } - std::array inverse_row_sums{}; + const Matrix lower = std::move(factor.Value().Lower().Value()); double reconstruction_error = 0.0; double block_frobenius = 0.0; Matrix transpose{kPhysical, kPhysical}; @@ -526,16 +412,9 @@ Status CheckPhysicalMass(const Matrix& mass, return fail("invalid-nonlinear-inertia-positive-mass", "Physical mass Cholesky reconstruction failed."); } - const Matrix inverse = - dense_blas_internal::MultiplyTransposeLeft(inverse_lower, inverse_lower); - for (std::size_t column = 0U; column < kPhysical; ++column) - for (std::size_t row = 0U; row < kPhysical; ++row) - inverse_row_sums[row] += std::abs(inverse(row, column)); - const double inverse_norm = - *std::max_element(inverse_row_sums.begin(), inverse_row_sums.end()); - const double reciprocal_condition = 1.0 / block_norm / inverse_norm; - if (!std::isfinite(inverse_norm) || !std::isfinite(reciprocal_condition) || - !(reciprocal_condition >= std::ldexp(1.0, -32))) { + const auto reciprocal_condition = factor.Value().ReciprocalConditionInfinity(); + if (!reciprocal_condition.HasValue() || + !(reciprocal_condition.Value() >= std::ldexp(1.0, -32))) { return fail("ill-conditioned-nonlinear-inertia-positive-mass", "Physical mass reciprocal infinity condition is below 2^-32."); } @@ -566,7 +445,7 @@ Result EvaluateDynamicRotation( Constant(initial_director.Z())}; const JetVector director_jet = Multiply(ExpSkew(phi), initial); const JetMatrix left_jacobian = LeftJacobian(phi); - JetVector null_jet = Multiply(Inverse(left_jacobian), director_jet); + JetVector null_jet = Solve(left_jacobian, director_jet); const Jet null_norm_squared = Dot(null_jet, null_jet); if (!std::isfinite(null_norm_squared.value) || !(null_norm_squared.value > 0.0)) { diff --git a/src/fesa/elements/nonlinear_mitc4_shell.cpp b/src/fesa/elements/nonlinear_mitc4_shell.cpp index 400d3e7..d058f18 100644 --- a/src/fesa/elements/nonlinear_mitc4_shell.cpp +++ b/src/fesa/elements/nonlinear_mitc4_shell.cpp @@ -6,12 +6,18 @@ #include #include #include +#include #include #include #include "fesa/elements/finite_rotation.h" #include "elements/nonlinear_mitc4_configuration_internal.h" #include "math/dense_blas_internal.h" +#include "math/jet_arithmetic.h" +#include "math/jet_linear_solve.h" +#include "math/finite_values.h" +#include "math/vector3_operations.h" +#include "elements/shell_kernel_values.h" namespace fesa { @@ -40,6 +46,15 @@ nonlinear_mitc4_internal::ConfigurationSnapshot::Read( namespace { +using shell_kernel_internal::Failure; +using shell_kernel_internal::FrameComponent; +using shell_kernel_internal::LocalEngineeringComponents; +using shell_kernel_internal::FrameMatrix; +using shell_kernel_internal::ElementIdentity; +using math_internal::DerivativeSum; +using math_internal::WeightedSum; +using math_internal::IsFinite; + constexpr std::size_t kNodeCount = 4U; constexpr double kFrameTolerance = 1.0e-12; constexpr double kDirectorOppositionTolerance = 1.0e-8; @@ -53,7 +68,7 @@ struct Geometry { std::array reference_covariant; std::array current_covariant; std::array covariant_increment; - std::array reciprocal; + std::optional reference_factor; Mitc4LocalFrame local_frame; double reference_jacobian; }; @@ -64,61 +79,6 @@ struct PointEvaluation { EngineeringStress stress; }; -/// @brief Builds one deterministic structured nonlinear-shell failure. -template -Result Failure(const std::string& code, const SourceLocation& location, - const std::string& identity, std::string message) { - return Result::Failure(Status::Failure( - FailureCategory::kModel, {{Severity::kError, code, location, "*ELEMENT", - identity, std::move(message)}})); -} - -/// @brief Builds the stable source label concatenation used by diagnostics. -std::string ElementIdentity(const std::array& nodes) { - std::string identity; - for (const Node* node : nodes) { - if (node == nullptr) { - continue; - } - if (!identity.empty()) { - identity += "-"; - } - identity += node->source_id.source_label_text; - } - return identity; -} - -/// @brief Forms a weighted vector sum in source-node order. -Vector3 WeightedSum(const std::array& weights, - const std::array& values) { - Vector3 result{}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - result = result + weights[node] * values[node]; - } - return result; -} - -/// @brief Forms a stable derivative sum after cancelling the constant term. -Vector3 DerivativeSum(const std::array& derivatives, - const std::array& values) { - std::array relative{}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - relative[node] = values[node] - values[0U]; - } - return WeightedSum(derivatives, relative); -} - -/// @brief Returns a frame matrix whose columns are a, b and the director. -Matrix FrameMatrix(const ShellNodeInitialFrame& frame) { - Matrix result{3U, 3U}; - for (std::size_t row = 0U; row < 3U; ++row) { - result(row, 0U) = frame.tangent_a[row]; - result(row, 1U) = frame.tangent_b[row]; - result(row, 2U) = frame.director[row]; - } - return result; -} - /// @brief Checks every scalar in a tensor before recovery commits it. bool IsFinite(const Tensor& tensor) { for (const auto& row : tensor) { @@ -131,13 +91,6 @@ bool IsFinite(const Tensor& tensor) { return true; } -/// @brief Checks every scalar in one fixed-size engineering quantity. -template -bool IsFinite(const std::array& values) { - return std::all_of(values.begin(), values.end(), - [](const double value) { return std::isfinite(value); }); -} - /// @brief Evaluates a determinant from covariant basis columns. double Determinant(const std::array& covariant) { return covariant[0U].Dot(covariant[1U].Cross(covariant[2U])); @@ -256,20 +209,24 @@ bool EvaluateGeometry(const std::array& coordinates, return false; } - geometry.reciprocal[0U] = - geometry.reference_covariant[1U].Cross(geometry.reference_covariant[2U]) / - geometry.reference_jacobian; - geometry.reciprocal[1U] = - geometry.reference_covariant[2U].Cross(geometry.reference_covariant[0U]) / - geometry.reference_jacobian; - geometry.reciprocal[2U] = - geometry.reference_covariant[0U].Cross(geometry.reference_covariant[1U]) / - geometry.reference_jacobian; - if (!geometry.reciprocal[0U].IsFinite() || - !geometry.reciprocal[1U].IsFinite() || - !geometry.reciprocal[2U].IsFinite()) { - return false; + // Retain the old unscaled cross-product finite gate before factor actions. + for (std::size_t alpha = 0; alpha < 3; ++alpha) + if (!geometry.reference_covariant[(alpha+1)%3].Cross( + geometry.reference_covariant[(alpha+2)%3]).IsFinite()) return false; + Matrix basis{3, 3}; + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t alpha = 0; alpha < 3; ++alpha) + basis(row, alpha) = geometry.reference_covariant[alpha][row]; + auto factor = dense_blas_internal::DenseFactor::Lu(basis); + if (!factor.HasValue()) return false; + // Stream the former reciprocal finite gate without retaining an inverse. + Matrix rhs{3, 1}; + for (std::size_t column = 0; column < 3; ++column) { + if (column != 0) rhs(column-1, 0) = 0; + rhs(column, 0) = 1; + if (!factor.Value().Solve(rhs).HasValue()) return false; } + geometry.reference_factor = std::move(factor.Value()); return MakeLocalFrame(reference_xi, reference_eta, initial_director, geometry.local_frame); } @@ -293,47 +250,27 @@ Tensor DirectStrain(const Geometry& geometry) { } /// @brief Reconstructs a fixed-reference Cartesian strain from covariant data. -Tensor CartesianStrain(const Tensor& covariant, - const std::array& reciprocal) { +Tensor CartesianStrain(const Tensor& covariant, const Geometry& reference) { + Matrix rhs{3, 3}; + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t column = 0; column < 3; ++column) + rhs(row, column) = covariant[row][column]; + const auto right = reference.reference_factor->SolveRight(rhs); Tensor cartesian{}; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - for (std::size_t first = 0U; first < 3U; ++first) { - for (std::size_t second = 0U; second < 3U; ++second) { - cartesian[row][column] += covariant[first][second] * - reciprocal[first][row] * - reciprocal[second][column]; - } - } + if (right.HasValue()) { + const auto left = reference.reference_factor->Solve(right.Value(), true); + if (left.HasValue()) { + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t column = 0; column < 3; ++column) + cartesian[row][column] = left.Value()(row, column); + return cartesian; } } + for (auto& row : cartesian) + row.fill(std::numeric_limits::quiet_NaN()); return cartesian; } -/// @brief Evaluates a tensor component in a fixed local reference frame. -double FrameComponent(const Vector3& left, const Tensor& tensor, - const Vector3& right) { - double value = 0.0; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - value += left[row] * tensor[row][column] * right[column]; - } - } - return value; -} - -/// @brief Converts fixed-reference Cartesian strain to engineering components. -EngineeringStrain LocalEngineering(const Tensor& tensor, - const Mitc4LocalFrame& frame) { - const Vector3 e1{frame.e1}; - const Vector3 e2{frame.e2}; - const Vector3 e3{frame.e3}; - return {FrameComponent(e1, tensor, e1), FrameComponent(e2, tensor, e2), - 2.0 * FrameComponent(e1, tensor, e2), - 2.0 * FrameComponent(e1, tensor, e3), - 2.0 * FrameComponent(e2, tensor, e3)}; -} - /// @brief Applies the approved homogeneous plane-stress plus shear law. EngineeringStress Stress(const EngineeringStrain& strain, const double youngs, const double poisson) { @@ -386,8 +323,8 @@ PointStatus EvaluatePoint(const std::array& coordinates, weights.eta_zeta[1U] * tying_samples[3U][1U][2U]; covariant[2U][1U] = covariant[1U][2U]; const Tensor cartesian = - CartesianStrain(covariant, point.geometry.reciprocal); - point.strain = LocalEngineering(cartesian, point.geometry.local_frame); + CartesianStrain(covariant, point.geometry); + point.strain = LocalEngineeringComponents(cartesian, point.geometry.local_frame); point.stress = Stress(point.strain, youngs, poisson); return IsFinite(covariant) && IsFinite(cartesian) && IsFinite(point.strain) && IsFinite(point.stress) @@ -890,27 +827,12 @@ JetMatrix3 Skew(const JetVector3& value) { /// @brief Multiplies two three-by-three Jet matrices. JetMatrix3 Multiply(const JetMatrix3& left, const JetMatrix3& right) { - JetMatrix3 result = ConstantZeroMatrix(); - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - for (std::size_t index = 0U; index < 3U; ++index) { - result[row][column] = - result[row][column] + left[row][index] * right[index][column]; - } - } - } - return result; + return math_internal::MultiplyJetMatrix(left, right, Constant); } /// @brief Multiplies a Jet matrix by a Jet vector. JetVector3 Multiply(const JetMatrix3& matrix, const JetVector3& vector) { - JetVector3 result{Constant(0.0), Constant(0.0), Constant(0.0)}; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - result[row] = result[row] + matrix[row][column] * vector[column]; - } - } - return result; + return math_internal::MultiplyJetMatrixVector(matrix, vector, Constant); } /// @brief Transposes a three-by-three Jet matrix. @@ -957,38 +879,28 @@ JetMatrix3 Add(const JetMatrix3& left, const JetMatrix3& right) { return result; } -/// @brief Computes the analytic three-by-three inverse through its adjugate. -JetMatrix3 Inverse(const JetMatrix3& matrix) { - JetMatrix3 cofactor{}; - cofactor[0U][0U] = - matrix[1U][1U] * matrix[2U][2U] - matrix[1U][2U] * matrix[2U][1U]; - cofactor[0U][1U] = - matrix[1U][2U] * matrix[2U][0U] - matrix[1U][0U] * matrix[2U][2U]; - cofactor[0U][2U] = - matrix[1U][0U] * matrix[2U][1U] - matrix[1U][1U] * matrix[2U][0U]; - cofactor[1U][0U] = - matrix[0U][2U] * matrix[2U][1U] - matrix[0U][1U] * matrix[2U][2U]; - cofactor[1U][1U] = - matrix[0U][0U] * matrix[2U][2U] - matrix[0U][2U] * matrix[2U][0U]; - cofactor[1U][2U] = - matrix[0U][1U] * matrix[2U][0U] - matrix[0U][0U] * matrix[2U][1U]; - cofactor[2U][0U] = - matrix[0U][1U] * matrix[1U][2U] - matrix[0U][2U] * matrix[1U][1U]; - cofactor[2U][1U] = - matrix[0U][2U] * matrix[1U][0U] - matrix[0U][0U] * matrix[1U][2U]; - cofactor[2U][2U] = - matrix[0U][0U] * matrix[1U][1U] - matrix[0U][1U] * matrix[1U][0U]; - const Jet determinant = matrix[0U][0U] * cofactor[0U][0U] + - matrix[0U][1U] * cofactor[0U][1U] + - matrix[0U][2U] * cofactor[0U][2U]; - JetMatrix3 result{}; - const Jet inverse_determinant = Reciprocal(determinant); - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - result[row][column] = cofactor[column][row] * inverse_determinant; - } - } - return result; +/// @brief Reads shell variations without conflating directional third order. +double ReadChannel(const Jet& jet, std::size_t degree, std::size_t channel) { + if (degree == 0) return jet.value; + if (degree == 1) return channel < kGlobalDofCount ? jet.first[channel] + : jet.directional_first; + if (degree == 3) return jet.mixed_second[channel]; + if (channel < kHessianSize) return jet.second.ReadData()[channel]; + if (channel == kHessianSize) return jet.directional_second; + return jet.mixed_first[channel - kHessianSize - 1]; +} +void WriteChannel(Jet& jet, std::size_t degree, std::size_t channel, + double value) { + if (degree == 0) { jet.value = value; return; } + // A null Hessian is not constant provenance, including zero first seeds. + jet.constant_derivatives = false; + if (degree == 1) { + if (channel < kGlobalDofCount) jet.first[channel] = value; + else jet.directional_first = value; + } else if (degree == 3) jet.mixed_second[channel] = value; + else if (channel < kHessianSize) jet.second.OverwriteData()[channel] = value; + else if (channel == kHessianSize) jet.directional_second = value; + else jet.mixed_first[channel - kHessianSize - 1] = value; } /// @brief Evaluates Rodrigues' map with an analytic near-zero series branch. @@ -1119,19 +1031,32 @@ JetTensor DirectStrain(const std::array& increment, /// @brief Reconstructs a fixed-reference Cartesian Jet strain tensor. JetTensor CartesianStrain(const JetTensor& covariant, const Geometry& reference) { - JetTensor result{}; - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - for (std::size_t first = 0U; first < 3U; ++first) { - for (std::size_t second = 0U; second < 3U; ++second) { - result[row][column] = - result[row][column] + reference.reciprocal[first][row] * - reference.reciprocal[second][column] * - covariant[first][second]; - } - } + // E_cart = B^-T E_cov B^-1. First solve each row against B, then each + // column against B^T, carrying every independent Jet channel unchanged. + const std::array channels{ + 1, kGlobalDofCount+1, kHessianSize+1+kGlobalDofCount, kGlobalDofCount}; + JetTensor right{}, result{}; + for (std::size_t row = 0; row < 3; ++row) { + if (!math_internal::SolveConstantJetAction( + covariant[row], *reference.reference_factor, true, channels, + Constant, ReadChannel, WriteChannel, right[row])) { + for (auto& entry : result) + entry.fill(Constant(std::numeric_limits::quiet_NaN())); + return result; } } + for (std::size_t column = 0; column < 3; ++column) { + const JetVector3 rhs{right[0][column],right[1][column],right[2][column]}; + JetVector3 action{}; + if (!math_internal::SolveConstantJetAction( + rhs, *reference.reference_factor, true, channels, + Constant, ReadChannel, WriteChannel, action)) { + for (auto& entry : result) + entry.fill(Constant(std::numeric_limits::quiet_NaN())); + return result; + } + for (std::size_t row = 0; row < 3; ++row) result[row][column] = action[row]; + } return result; } @@ -1235,7 +1160,29 @@ Jet PhysicalEnergy(const std::array& coordinates, JetMatrix3 PolarRotation(const JetMatrix3& deformation) { JetMatrix3 result = deformation; for (std::size_t iteration = 0U; iteration < 12U; ++iteration) { - result = Scale(0.5, Add(result, Transpose(Inverse(result)))); + const JetMatrix3 coefficients = Transpose(result); + Matrix primal{3, 3}; + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t column = 0; column < 3; ++column) + primal(row, column) = coefficients[row][column].value; + const auto factor = dense_blas_internal::DenseFactor::Lu(primal); + JetMatrix3 candidate{}; + for (std::size_t column = 0; column < 3; ++column) { + JetVector3 rhs{Constant(0), Constant(0), Constant(0)}, action{}; + rhs[column] = Constant(1); + if (!factor.HasValue() || !math_internal::SolveJetAction( + coefficients, rhs, factor.Value(), + {1, kGlobalDofCount + 1, kHessianSize + 1 + kGlobalDofCount, + kGlobalDofCount}, Constant, ReadChannel, WriteChannel, action)) { + for (auto& row : candidate) + row.fill(Constant(std::numeric_limits::quiet_NaN())); + return candidate; + } + for (std::size_t row = 0; row < 3; ++row) + candidate[row][column] = 0.5 * (result[row][column] + action[row]); + } + // All actions used the previous snapshot; never a partial next iterate. + result = std::move(candidate); } return result; } @@ -1297,14 +1244,19 @@ Jet DrillingEnergy(const std::array& coordinates, const auto current = CovariantIncrement(initial_directors, trial, thickness, 0.0, 0.0, 0.0); JetMatrix3 deformation = Identity(); - for (std::size_t row = 0U; row < 3U; ++row) { - for (std::size_t column = 0U; column < 3U; ++column) { - for (std::size_t alpha = 0U; alpha < 3U; ++alpha) { - deformation[row][column] = - deformation[row][column] + - current[alpha][row] * reference.reciprocal[alpha][column]; - } + const std::array channels{ + 1, kGlobalDofCount+1, kHessianSize+1+kGlobalDofCount, kGlobalDofCount}; + for (std::size_t row = 0; row < 3; ++row) { + const JetVector3 rhs{current[0][row],current[1][row],current[2][row]}; + JetVector3 action{}; + if (!math_internal::SolveConstantJetAction( + rhs, *reference.reference_factor, true, channels, + Constant, ReadChannel, WriteChannel, action)) { + valid = false; + return {}; } + for (std::size_t column = 0; column < 3; ++column) + deformation[row][column] = deformation[row][column] + action[column]; } const JetMatrix3 element_rotation = PolarRotation(deformation); Jet energy{}; diff --git a/src/fesa/elements/shell_kernel_values.h b/src/fesa/elements/shell_kernel_values.h new file mode 100644 index 0000000..9c0b761 --- /dev/null +++ b/src/fesa/elements/shell_kernel_values.h @@ -0,0 +1,79 @@ +#ifndef FESA_ELEMENTS_SHELL_KERNEL_VALUES_H_ +#define FESA_ELEMENTS_SHELL_KERNEL_VALUES_H_ + +#include +#include +#include +#include + +#include "fesa/elements/mitc4_shell.h" +#include "fesa/math/matrix.h" +#include "fesa/math/vector3.h" +#include "fesa/model/model_types.h" + +namespace fesa::shell_kernel_internal { + +/// @brief Constructs the shared model-category shell-kernel failure envelope. +template +Result Failure(const std::string& code, const SourceLocation& location, + const std::string& identity, std::string message) { + return Result::Failure(Status::Failure( + FailureCategory::kModel, {{Severity::kError, code, location, "*ELEMENT", + identity, std::move(message)}})); +} + +/// @brief Preserves raw source labels and the existing null-skip diagnostic identity. +inline std::string ElementIdentity(const std::array& nodes) { + std::string identity; + for (const Node* node : nodes) { + if (node == nullptr) { + continue; + } + if (!identity.empty()) { + identity += "-"; + } + identity += node->source_id.source_label_text; + } + return identity; +} + +/// @brief Copies the initial semantic frame into its three ordered columns. +inline Matrix FrameMatrix(const ShellNodeInitialFrame& frame) { + Matrix result{3U, 3U}; + for (std::size_t row = 0U; row < 3U; ++row) { + result(row, 0U) = frame.tangent_a[row]; + result(row, 1U) = frame.tangent_b[row]; + result(row, 2U) = frame.director[row]; + } + return result; +} + +/// @brief Contracts one ordinary tensor component in the original row/column order. +inline double FrameComponent( + const Vector3& left, const std::array, 3>& tensor, + const Vector3& right) { + double value = 0.0; + for (std::size_t row = 0U; row < 3U; ++row) { + for (std::size_t column = 0U; column < 3U; ++column) { + value += left[row] * tensor[row][column] * right[column]; + } + } + return value; +} + +/// @brief Projects fixed-reference tensor strain to the five engineering components. +inline std::array LocalEngineeringComponents( + const std::array, 3>& tensor, + const Mitc4LocalFrame& frame) { + const Vector3 e1{frame.e1}; + const Vector3 e2{frame.e2}; + const Vector3 e3{frame.e3}; + return {FrameComponent(e1, tensor, e1), FrameComponent(e2, tensor, e2), + 2.0 * FrameComponent(e1, tensor, e2), + 2.0 * FrameComponent(e1, tensor, e3), + 2.0 * FrameComponent(e2, tensor, e3)}; +} + +} // namespace fesa::shell_kernel_internal + +#endif // FESA_ELEMENTS_SHELL_KERNEL_VALUES_H_ diff --git a/src/fesa/elements/thermal_shell_geometry.cpp b/src/fesa/elements/thermal_shell_geometry.cpp index 8219714..ac21a9a 100644 --- a/src/fesa/elements/thermal_shell_geometry.cpp +++ b/src/fesa/elements/thermal_shell_geometry.cpp @@ -12,6 +12,7 @@ #include #include "fesa/model/model_types.h" +#include "math/dense_blas_internal.h" namespace fesa { namespace thermal_internal { @@ -136,10 +137,20 @@ Result BuildGeometry(const std::array& coordinates, const double uv = u.Dot(v); const double determinant = uu * vv - uv * uv; if (determinant > 0 && std::isfinite(determinant)) { - const double xi = (uv * v.Dot(w0) - vv * u.Dot(w0)) / determinant; - const double eta = (uv * u.Dot(w0) - uu * v.Dot(w0)) / determinant; - if (std::abs(xi) <= 1 && std::abs(eta) <= 1) { - candidates.push_back(w0 + xi * u + eta * v); + Matrix metric{2, 2}; + metric(0,0) = uu; metric(0,1) = uv; + metric(1,0) = uv; metric(1,1) = vv; + Matrix rhs{2, 1}; + rhs(0,0) = -u.Dot(w0); rhs(1,0) = -v.Dot(w0); + const auto factor = dense_blas_internal::DenseFactor::Lu(metric); + if (factor.HasValue()) { + const auto stationary = factor.Value().Solve(rhs); + if (stationary.HasValue()) { + const double xi = stationary.Value()(0,0); + const double eta = stationary.Value()(1,0); + if (std::abs(xi) <= 1 && std::abs(eta) <= 1) + candidates.push_back(w0 + xi * u + eta * v); + } } } // The center is also useful for affine or rank-one normal images. @@ -169,30 +180,39 @@ Result EvaluateSurface(const Geometry& geometry, double xi, const double jacobian = w.Norm(); const Interval determinant_interval = NormSquared(wi); const double determinant = w.Dot(w); - // |a1 x a2|^2 is the Gram determinant identity, avoiding subtraction of - // nearly equal g11*g22 and g12*g12. This is still the actual-point metric. - const Interval inverse11 = Dot(a2i, a2i) / determinant_interval; - const Interval inverse12 = - (Interval{0} - Dot(a1i, a2i)) / determinant_interval; - const Interval inverse22 = Dot(a1i, a1i) / determinant_interval; - if (!(determinant_interval.lower > 0) || !inverse11.IsFinite() || - !inverse12.IsFinite() || !inverse22.IsFinite() || !(jacobian > 0) || + // Retain the original scalar range/scale certificate. Each canonical + // metric action is consumed immediately by its finite gate, never retained + // as inverse coefficients or used to compute a physical gradient. + const auto finite_metric_action = [&](Interval numerator) { + const Interval action = numerator / determinant_interval; + return action.IsFinite() && + Scale(action, -2 * geometry.length_exponent).IsFinite(); + }; + if (!(determinant_interval.lower > 0) || + !finite_metric_action(Dot(a2i, a2i)) || + !finite_metric_action(Interval{0} - Dot(a1i, a2i)) || + !finite_metric_action(Dot(a1i, a1i)) || !(jacobian > 0) || !std::isfinite(determinant) || determinant <= 0 || - !Scale(inverse11, -2 * geometry.length_exponent).IsFinite() || - !Scale(inverse12, -2 * geometry.length_exponent).IsFinite() || - !Scale(inverse22, -2 * geometry.length_exponent).IsFinite() || !Scale(Dot(a1i, a1i), 2 * geometry.length_exponent).IsFinite() || !Scale(Dot(a2i, a2i), 2 * geometry.length_exponent).IsFinite()) { return Result::Failure(GeometryFailure( source, location, "uncertain-thermal-shell-metric", "Metric inverse enclosure is not finite or excludes no zero divisor.")); } - const Vector3 reciprocal1 = a2.Cross(w) / determinant; - const Vector3 reciprocal2 = w.Cross(a1) / determinant; - const auto reciprocal1i = - (Interval{1} / determinant_interval) * Cross(a2i, wi); - const auto reciprocal2i = - (Interval{1} / determinant_interval) * Cross(wi, a1i); + // Factor the physical tangent constraints rather than the rounded Gram + // matrix, which can lose rank while the certified patch is still regular. + Matrix constraints{3, 3}; + for (std::size_t j = 0; j < 3; ++j) { + constraints(0,j) = a1[j]; + constraints(1,j) = a2[j]; + constraints(2,j) = w[j] / jacobian; + } + const auto factor = dense_blas_internal::DenseFactor::Lu(constraints); + if (!factor.HasValue()) + return Result::Failure(GeometryFailure( + source, location, "uncertain-thermal-shell-metric", + "Surface gradient enclosure is not finite.")); + const Interval reciprocal_determinant = Interval{1} / determinant_interval; SurfacePoint point{}; point.jacobian = std::ldexp(jacobian, 2 * geometry.length_exponent); point.jacobian_interval = @@ -213,13 +233,26 @@ Result EvaluateSurface(const Geometry& geometry, double xi, (Interval{1} + Interval{kEta[i]} * eta_interval); const Interval dyi = Interval{.25 * kEta[i]} * (Interval{1} + Interval{kXi[i]} * xi_interval); - const Vector3 gradient = dx * reciprocal1 + dy * reciprocal2; - const auto gradient_interval = dxi * reciprocal1i + dyi * reciprocal2i; + Matrix rhs{3, 1}; + rhs(0,0) = dx; rhs(1,0) = dy; + const auto gradient = factor.Value().Solve(rhs); + if (!gradient.HasValue()) + return Result::Failure(GeometryFailure( + source, location, "uncertain-thermal-shell-metric", + "Surface gradient enclosure is not finite.")); + // The real solution obeys a1.g=dx, a2.g=dy, w.g=0. Its exterior-product + // identity encloses the solution directly. The original outward endpoint + // graph is retained component by component; no interval factor pivot or + // interval inverse/basis is introduced and no low-order term is discarded. + const auto first_numerator = Cross(a2i, wi); + const auto second_numerator = Cross(wi, a1i); std::array components{}; for (std::size_t j = 0; j < 3; ++j) { - components[j] = std::ldexp(gradient[j], -geometry.length_exponent); + components[j] = std::ldexp(gradient.Value()(j,0), -geometry.length_exponent); point.gradient_interval[i][j] = - Scale(gradient_interval[j], -geometry.length_exponent); + Scale(dxi * (reciprocal_determinant * first_numerator[j]) + + dyi * (reciprocal_determinant * second_numerator[j]), + -geometry.length_exponent); } point.gradient[i] = Vector3{components}; if (!point.gradient[i].IsFinite() || diff --git a/src/fesa/elements/thermal_shell_geometry.h b/src/fesa/elements/thermal_shell_geometry.h index a31b339..bf007c7 100644 --- a/src/fesa/elements/thermal_shell_geometry.h +++ b/src/fesa/elements/thermal_shell_geometry.h @@ -39,7 +39,7 @@ struct SurfacePoint { Result BuildGeometry(const std::array& coordinates, const SourceEntityId& source, const SourceLocation& location); -/// @brief Checks finite metric inverse and gradients at one used surface point. +/// @brief Solves actual tangential-gradient RHS and certifies their enclosures. Result EvaluateSurface(const Geometry& geometry, double xi, double eta, Interval xi_interval, Interval eta_interval, diff --git a/src/fesa/fem/dof_manager.cpp b/src/fesa/fem/dof_manager.cpp index 17ac70a..a945bef 100644 --- a/src/fesa/fem/dof_manager.cpp +++ b/src/fesa/fem/dof_manager.cpp @@ -11,6 +11,8 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/elements/element.h" #include "fesa/model/source_target_resolver.h" namespace fesa { @@ -26,13 +28,6 @@ Status DofFailure(const std::string& code, const SourceLocation& location, {{Severity::kError, code, location, "DOF_MANAGER", identity, message}}); } -bool SameSourceIdentity(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; -} - bool IsStrictlyIncreasing(const std::vector& values) { return std::adjacent_find( values.begin(), values.end(), @@ -220,7 +215,7 @@ Status DofManager::BuildThermal( candidate.element_scatters_.reserve(layouts.size()); for (std::size_t index = 0U; index < layouts.size(); ++index) { const auto& definition = domain.ThermalElements()[index]; - if (!SameSourceIdentity(layouts[index].source_id, definition.SourceId())) { + if (!SourceIdentityEquals(layouts[index].source_id, definition.SourceId())) { return DofFailure( "invalid-element-layout-order", {domain.SourcePath(), 0U}, std::to_string(index), "Thermal layout identity changed source order."); @@ -323,7 +318,7 @@ Status DofManager::BuildLayouts(const AnalysisModel& analysis_model, } } if (definition_index >= domain.Elements().Size() || - !SameSourceIdentity(layouts[source_order].source_id, + !SourceIdentityEquals(layouts[source_order].source_id, domain.Elements()[definition_index].SourceId())) { return DofFailure( "invalid-element-layout-order", {domain.SourcePath(), 0U}, diff --git a/src/fesa/io/abaqus/domain_mapping_context.cpp b/src/fesa/io/abaqus/domain_mapping_context.cpp index 4d5b046..941bf3f 100644 --- a/src/fesa/io/abaqus/domain_mapping_context.cpp +++ b/src/fesa/io/abaqus/domain_mapping_context.cpp @@ -7,25 +7,22 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include -#include "fesa/analysis/thermal_time_grid.h" #include "fesa/core/ascii.h" -#include "fesa/math/vector3.h" -#include "fesa/model/shell_geometry.h" -#include "fesa/model/source_target_resolver.h" +#include "io/abaqus/mapping_diagnostics.h" +#include "io/abaqus/material_property_candidates.h" +#include "io/abaqus/step_definition_candidates.h" +#include "io/abaqus/surface_definition_candidates.h" +#include "io/abaqus/topology_candidates.h" namespace fesa::abaqus_internal { -namespace { - std::vector WithoutTrailingEmpty(std::vector fields) { while (!fields.empty() && fields.back().empty()) { fields.pop_back(); @@ -33,183 +30,214 @@ std::vector WithoutTrailingEmpty(std::vector fields) { return fields; } -std::string CanonicalizeAscii(std::string value) { - for (char& character : value) { - if (character >= 'a' && character <= 'z') { - character = static_cast(character - 'a' + 'A'); - } +void MappingDiagnostics::RecordFailure(const Status& status) { + if (!failure_) { + failure_ = + MappingFailure{status.Category().value_or(FailureCategory::kModel), + status.Diagnostics().front()}; } - return value; } -struct RawNode { - std::int64_t label; - std::string label_text; - std::array coordinates; - SourceLocation location; -}; +bool MappingDiagnostics::ParseInteger(const std::string& text, + std::int64_t& value, + const SourceLocation& location, + const std::string& keyword, + bool positive) { + if (text.empty()) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "A numeric field cannot be empty."); + } + char* end = nullptr; + errno = 0; + const long long parsed = std::strtoll(text.c_str(), &end, 10); + if (errno == ERANGE || end != text.c_str() + text.size() || + (positive && parsed <= 0)) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "The field must be a valid positive base-10 integer."); + } + value = static_cast(parsed); + return true; +} -struct RawElement { - enum class Type { kB33, kS4, kS4r, kDs4, kC3d8 }; +void MappingDiagnostics::RejectUnknown(const KeywordBlock& block, + bool shell_model) { + if (block.canonical_name == "DLOAD" && shell_model) { + InputFailure("unsupported-distributed-load", block.location, + block.canonical_name, "", + "Distributed, pressure, gravity, body, edge, and follower " + "loads are unsupported."); + return; + } + InputFailure("unsupported-keyword", block.location, block.canonical_name, "", + "The keyword is outside the approved Abaqus subset."); +} - std::int64_t label; - std::string label_text; - Type type; - std::vector node_labels; - SourceLocation location; -}; +Status MappingDiagnostics::CurrentStatus() const { + if (!failure_) { + return Status::Ok(); + } + return Status::Failure(failure_->category, {failure_->diagnostic}); +} -enum class ElementFamily { kBeam, kShell, kThermalShell, kSolid }; +const KeywordParameter* MappingDiagnostics::Parameter( + const KeywordBlock& block, std::string_view name) const { + const auto found = + std::find_if(block.parameters.begin(), block.parameters.end(), + [name](const KeywordParameter& candidate) { + return candidate.name == name; + }); + return found == block.parameters.end() ? nullptr : &*found; +} -struct RawSet { - std::string name; - std::vector members; - SourceLocation location; -}; +bool MappingDiagnostics::Fail(FailureCategory category, std::string code, + const SourceLocation& location, + std::string keyword, std::string entity_identity, + std::string message) { + if (!failure_) { + failure_ = MappingFailure{ + category, + {Severity::kError, std::move(code), location, std::move(keyword), + std::move(entity_identity), std::move(message)}}; + } + return false; +} -struct RawSection { - std::string element_set_name; - std::string material_name; - std::array properties; - std::array first_axis; - std::vector> section_points; - SourceLocation location; -}; +bool MappingDiagnostics::InputFailure(std::string code, + const SourceLocation& location, + std::string keyword, + std::string entity_identity, + std::string message) { + return Fail(FailureCategory::kInput, std::move(code), location, + std::move(keyword), std::move(entity_identity), + std::move(message)); +} -struct RawShellSection { - std::string element_set_name; - std::string material_name; - double thickness; - SourceLocation location; -}; +bool MappingDiagnostics::ModelFailure(std::string code, + const SourceLocation& location, + std::string keyword, + std::string entity_identity, + std::string message) { + return Fail(FailureCategory::kModel, std::move(code), location, + std::move(keyword), std::move(entity_identity), + std::move(message)); +} -struct RawSolidSection { - std::string element_set_name; - std::string material_name; - SourceLocation location; -}; +bool MappingDiagnostics::InvalidKeywordLocation(const KeywordBlock& block, + std::string message) { + return InputFailure("invalid-keyword-location", block.location, + block.canonical_name, "", std::move(message)); +} -struct RawPart { - std::string name; - SourceLocation location; - std::vector nodes; - std::vector elements; - std::vector node_sets; - std::vector element_sets; - std::vector sections; - std::vector shell_sections; - std::vector solid_sections; -}; +bool MappingDiagnostics::ValidateParameters( + const KeywordBlock& block, const std::vector& allowed, + std::string_view unsupported_code) { + std::set seen; + for (const auto& candidate : block.parameters) { + if (!seen.insert(candidate.name).second) { + return InputFailure("duplicate-entity", block.location, + block.canonical_name, candidate.name, + "A keyword parameter may be declared only once."); + } + if (std::find(allowed.begin(), allowed.end(), candidate.name) == + allowed.end()) { + return InputFailure(std::string{unsupported_code}, block.location, + block.canonical_name, candidate.name, + "The keyword parameter is outside the approved " + "subset."); + } + } + return true; +} -struct RawInstance { - std::string name; - std::string part_name; - SourceLocation location; -}; +const std::string* MappingDiagnostics::RequiredParameterValue( + const KeywordBlock& block, std::string_view name) { + const auto* found = Parameter(block, name); + if (found == nullptr || !found->value || found->value->empty()) { + InputFailure("invalid-keyword-parameter", block.location, + block.canonical_name, std::string{name}, + "The keyword requires a non-empty parameter value."); + return nullptr; + } + return &*found->value; +} -struct RawAssemblySet { - bool is_node_set; - std::string name; - std::string instance_name; - std::vector members; - SourceLocation location; -}; +bool MappingDiagnostics::RequireNoData(const KeywordBlock& block) { + if (!block.data.empty()) { + return InputFailure("invalid-data-arity", block.data.front().location, + block.canonical_name, "", + "This keyword does not accept data rows."); + } + return true; +} -struct RawSurfaceMember { - std::string target; - ShellSurfaceSide side; - SourceLocation location; - std::size_t ordinal; -}; +bool MappingDiagnostics::ParseDouble(const std::string& text, double& value, + const SourceLocation& location, + const std::string& keyword) { + if (text.empty()) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "A numeric field cannot be empty."); + } + char* end = nullptr; + errno = 0; + const double parsed = std::strtod(text.c_str(), &end); + if (errno == ERANGE || end != text.c_str() + text.size() || + !std::isfinite(parsed)) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "The field must be a finite floating-point value."); + } + value = parsed; + return true; +} -struct RawElementSurface { - std::string name; - SourceLocation location; - std::size_t declaration_ordinal; - std::vector members; -}; +bool MappingDiagnostics::ParseThermalDouble(const std::string& text, + double& value, + const SourceLocation& location, + const std::string& keyword) { + if (text.empty() || + (text.front() == '+' && + (text.size() == 1U || text[1U] == '+' || text[1U] == '-'))) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "A thermal scalar requires finite decimal syntax."); + } + const char* begin = text.data() + (text.front() == '+' ? 1U : 0U); + const char* end = text.data() + text.size(); + const auto parsed = + std::from_chars(begin, end, value, std::chars_format::general); + if (parsed.ec != std::errc{} || parsed.ptr != end || !std::isfinite(value)) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "A thermal scalar must be representable and finite."); + } + return true; +} -struct RawSurfacePressureLoad { - std::string surface_name; - double magnitude; - std::string magnitude_raw; - SourceLocation keyword_location; - SourceLocation data_location; - std::size_t data_ordinal; - std::size_t load_ordinal; -}; - -struct RawMaterial { - std::string name; - double youngs_modulus{0.0}; - double poisson_ratio{0.0}; - bool has_elastic{false}; - std::optional density; - std::optional conductivity; - std::optional specific_heat; - SourceLocation location; - SourceLocation elastic_location; - SourceLocation density_location; -}; - -struct RawThermalBoundary { - std::string target; - int dof; - double value; - SourceLocation location; -}; - -struct RawThermalInitial { - std::string target; - double value; - SourceLocation location; -}; - -struct RawThermalFlux { - std::string target; - int dof; - double power; - SourceLocation location; -}; - -struct RawStep { - SourceLocation location; - std::string name_raw; - std::string name_canonical; - bool has_static{false}; - bool static_direct{false}; - std::uint32_t static_increment_count{0U}; - std::optional dynamic; - bool perturbation_explicit{false}; - bool nlgeom_explicit{false}; - StaticProcedureKind procedure_kind{ - StaticProcedureKind::kLinearSmallDisplacement}; - bool nlgeom{false}; - std::uint32_t maximum_increments{0U}; - std::array static_values{}; - std::optional frequency; - bool has_heat_transfer{false}; - double thermal_increment{0.0}; - double thermal_period{0.0}; - std::vector thermal_boundaries; - std::vector thermal_fluxes; - std::vector boundaries; - std::vector loads; - std::vector surface_pressure_loads; - std::size_t next_load_ordinal{0U}; -}; - -struct MappingFailure { - FailureCategory category; - Diagnostic diagnostic; -}; - -} // namespace +bool MappingDiagnostics::ParseModelDouble(const std::string& text, + double& value, + const SourceLocation& location, + const std::string& keyword, + const std::string& diagnostic_code, + const std::string& message) { + if (text.empty()) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "A numeric field cannot be empty."); + } + char* end = nullptr; + errno = 0; + const double parsed = std::strtod(text.c_str(), &end); + if (end != text.c_str() + text.size()) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "The field must use floating-point syntax."); + } + if (errno == ERANGE || !std::isfinite(parsed)) { + return ModelFailure(diagnostic_code, location, keyword, text, message); + } + value = parsed; + return true; +} /// @brief Builds one complete Domain candidate while retaining source identity. /// @note The candidate is committed only after all blocks and cross-references /// have been validated. -class DomainMappingContext::Impl { +class DomainMappingContext::Impl : private MappingDiagnostics { public: explicit Impl(const ParsedInput& input) : input_{input} {} @@ -224,52 +252,36 @@ class DomainMappingContext::Impl { } ValidateCardinality(); if (!failure_) { - FinalizeMaterials(); + material_properties_.FinalizeMaterials(definition_); } if (!failure_) { - FinalizePartsAndSections(); + material_properties_.FinalizePartsAndSections(topology_.Parts(), + definition_); } if (!failure_) { - FinalizeDensities(); + material_properties_.FinalizeDensities( + steps_.MaterialProcedure(surfaces_.FirstSurface(), + physical_constants_location_), + definition_); } return CurrentStatus(); } Status FinalizeTopology() { - if (failure_) { - return CurrentStatus(); - } - ExpandInstances(); - if (!failure_ && !definition_.shell_elements.empty()) { - auto geometry = - PreprocessShellGeometry(definition_.nodes, definition_.shell_elements, - definition_.shell_sections); - if (!geometry.HasValue()) { - const auto& status = geometry.GetStatus(); - failure_ = - MappingFailure{status.Category().value_or(FailureCategory::kModel), - status.Diagnostics().front()}; - } else { - definition_.shell_node_initial_frames = - std::move(geometry.Value().nodal_frames); - } - } - if (!failure_) { - ExpandAssemblySets(); - } + topology_.Finalize(material_properties_, definition_); return CurrentStatus(); } Status FinalizeElementSurfaces() { if (!failure_) { - ResolveElementSurfaces(); + surfaces_.ResolveElementSurfaces(); } return CurrentStatus(); } Status FinalizeStepCandidate() { if (!failure_) { - FinalizeStep(); + steps_.FinalizeStep(); } return CurrentStatus(); } @@ -285,22 +297,19 @@ class DomainMappingContext::Impl { DomainMappingCandidateSummary CandidateSummary() const noexcept { DomainMappingCandidateSummary summary{}; - summary.part_count = parts_.size(); - summary.instance_count = instances_.size(); - summary.material_count = materials_.size(); - summary.model_boundary_count = model_boundaries_.size(); - summary.step_boundary_count = step_.boundaries.size(); - summary.step_load_count = step_.loads.size(); - summary.has_static_step = step_.has_static; - for (const auto& part : parts_) { + summary.part_count = topology_.Parts().size(); + summary.instance_count = topology_.Instances().size(); + summary.material_count = material_properties_.Count(); + steps_.FillSummary(summary); + for (const auto& part : topology_.Parts()) { summary.node_count += part.nodes.size(); summary.element_count += part.elements.size(); summary.node_set_count += part.node_sets.size(); summary.element_set_count += part.element_sets.size(); - summary.beam_section_count += part.sections.size(); - summary.shell_section_count += part.shell_sections.size(); + summary.beam_section_count += part.properties.sections.size(); + summary.shell_section_count += part.properties.shell_sections.size(); } - for (const auto& set : assembly_sets_) { + for (const auto& set : topology_.AssemblySets()) { if (set.is_node_set) { ++summary.node_set_count; } else { @@ -311,282 +320,6 @@ class DomainMappingContext::Impl { } private: - Status CurrentStatus() const { - if (!failure_) { - return Status::Ok(); - } - return Status::Failure(failure_->category, {failure_->diagnostic}); - } - - const KeywordParameter* Parameter(const KeywordBlock& block, - std::string_view name) const { - const auto found = - std::find_if(block.parameters.begin(), block.parameters.end(), - [name](const KeywordParameter& candidate) { - return candidate.name == name; - }); - return found == block.parameters.end() ? nullptr : &*found; - } - - bool Fail(FailureCategory category, std::string code, - const SourceLocation& location, std::string keyword, - std::string entity_identity, std::string message) { - if (!failure_) { - failure_ = MappingFailure{ - category, - {Severity::kError, std::move(code), location, std::move(keyword), - std::move(entity_identity), std::move(message)}}; - } - return false; - } - - bool InputFailure(std::string code, const SourceLocation& location, - std::string keyword, std::string entity_identity, - std::string message) { - return Fail(FailureCategory::kInput, std::move(code), location, - std::move(keyword), std::move(entity_identity), - std::move(message)); - } - - bool ModelFailure(std::string code, const SourceLocation& location, - std::string keyword, std::string entity_identity, - std::string message) { - return Fail(FailureCategory::kModel, std::move(code), location, - std::move(keyword), std::move(entity_identity), - std::move(message)); - } - - bool InvalidKeywordLocation(const KeywordBlock& block, std::string message) { - return InputFailure("invalid-keyword-location", block.location, - block.canonical_name, "", std::move(message)); - } - - bool ValidateParameters( - const KeywordBlock& block, const std::vector& allowed, - std::string_view unsupported_code = "invalid-keyword-parameter") { - std::set seen; - for (const auto& candidate : block.parameters) { - if (!seen.insert(candidate.name).second) { - return InputFailure("duplicate-entity", block.location, - block.canonical_name, candidate.name, - "A keyword parameter may be declared only once."); - } - if (std::find(allowed.begin(), allowed.end(), candidate.name) == - allowed.end()) { - return InputFailure(std::string{unsupported_code}, block.location, - block.canonical_name, candidate.name, - "The keyword parameter is outside the approved " - "subset."); - } - } - return true; - } - - const std::string* RequiredParameterValue(const KeywordBlock& block, - std::string_view name) { - const auto* found = Parameter(block, name); - if (found == nullptr || !found->value || found->value->empty()) { - InputFailure("invalid-keyword-parameter", block.location, - block.canonical_name, std::string{name}, - "The keyword requires a non-empty parameter value."); - return nullptr; - } - return &*found->value; - } - - bool RequireNoData(const KeywordBlock& block) { - if (!block.data.empty()) { - return InputFailure("invalid-data-arity", block.data.front().location, - block.canonical_name, "", - "This keyword does not accept data rows."); - } - return true; - } - - bool ParseInteger(const std::string& text, std::int64_t& value, - const SourceLocation& location, const std::string& keyword, - bool positive) { - if (text.empty()) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "A numeric field cannot be empty."); - } - char* end = nullptr; - errno = 0; - const long long parsed = std::strtoll(text.c_str(), &end, 10); - if (errno == ERANGE || end != text.c_str() + text.size() || - (positive && parsed <= 0)) { - return InputFailure( - "invalid-numeric-value", location, keyword, text, - "The field must be a valid positive base-10 integer."); - } - value = static_cast(parsed); - return true; - } - - bool ParseDouble(const std::string& text, double& value, - const SourceLocation& location, const std::string& keyword) { - if (text.empty()) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "A numeric field cannot be empty."); - } - char* end = nullptr; - errno = 0; - const double parsed = std::strtod(text.c_str(), &end); - if (errno == ERANGE || end != text.c_str() + text.size() || - !std::isfinite(parsed)) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "The field must be a finite floating-point value."); - } - value = parsed; - return true; - } - - /// @brief Parses finite thermal decimal scalars without strtod underflow - /// flags. - /// @note from_chars preserves representable subnormals and rejects values - /// outside binary64 range; structural scalar parsing remains unchanged. - bool ParseThermalDouble(const std::string& text, double& value, - const SourceLocation& location, - const std::string& keyword) { - if (text.empty() || - (text.front() == '+' && - (text.size() == 1U || text[1U] == '+' || text[1U] == '-'))) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "A thermal scalar requires finite decimal syntax."); - } - const char* begin = text.data() + (text.front() == '+' ? 1U : 0U); - const char* end = text.data() + text.size(); - const auto parsed = - std::from_chars(begin, end, value, std::chars_format::general); - if (parsed.ec != std::errc{} || parsed.ptr != end || - !std::isfinite(value)) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "A thermal scalar must be representable and finite."); - } - return true; - } - - bool ParseSurfacePressureDouble(const std::string& text, double& value, - const SourceLocation& location, - const std::string& keyword) { - std::size_t cursor = 0U; - if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { - ++cursor; - } - const std::size_t integer_start = cursor; - while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { - ++cursor; - } - const bool has_integer_digits = cursor != integer_start; - bool has_fraction_digits = false; - if (cursor < text.size() && text[cursor] == '.') { - ++cursor; - const std::size_t fraction_start = cursor; - while (cursor < text.size() && text[cursor] >= '0' && - text[cursor] <= '9') { - ++cursor; - } - has_fraction_digits = cursor != fraction_start; - } - if (!has_integer_digits && !has_fraction_digits) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "Pressure values require C-locale decimal syntax."); - } - if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) { - ++cursor; - if (cursor < text.size() && - (text[cursor] == '+' || text[cursor] == '-')) { - ++cursor; - } - const std::size_t exponent_start = cursor; - while (cursor < text.size() && text[cursor] >= '0' && - text[cursor] <= '9') { - ++cursor; - } - if (cursor == exponent_start) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "Pressure exponents require one or more digits."); - } - } - if (cursor != text.size()) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "Pressure values require C-locale decimal syntax."); - } - const char* const begin = text.data(); - const char* const end = begin + text.size(); - const char* const numeric_begin = *begin == '+' ? begin + 1U : begin; - const auto parsed = - std::from_chars(numeric_begin, end, value, std::chars_format::general); - if (parsed.ec != std::errc{} || parsed.ptr != end || - !std::isfinite(value)) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "Pressure values must be finite C-locale values."); - } - return true; - } - - bool ParseNonlinearStaticDouble(const std::string& text, double& value, - const SourceLocation& location, - const std::string& keyword) { - if (text.empty()) { - return InputFailure("invalid-nonlinear-static-data", location, keyword, - text, "A nonlinear STATIC field cannot be empty."); - } - char* end = nullptr; - errno = 0; - const double parsed = std::strtod(text.c_str(), &end); - if (errno == ERANGE || end != text.c_str() + text.size() || - !std::isfinite(parsed)) { - return InputFailure("invalid-nonlinear-static-data", location, keyword, - text, - "All four nonlinear STATIC fields must be finite."); - } - value = parsed; - return true; - } - - bool ParseDynamicDouble(const std::string& text, double& value, - const SourceLocation& location, - const std::string& keyword) { - if (text.empty()) { - return InputFailure("invalid-dynamic-data", location, keyword, text, - "Dynamic data fields cannot be empty."); - } - char* end = nullptr; - errno = 0; - const double parsed = std::strtod(text.c_str(), &end); - if (errno == ERANGE || end != text.c_str() + text.size() || - !std::isfinite(parsed)) { - return InputFailure("invalid-dynamic-data", location, keyword, text, - "Dynamic data fields must be finite."); - } - value = parsed; - return true; - } - - bool ParseModelDouble(const std::string& text, double& value, - const SourceLocation& location, - const std::string& keyword, - const std::string& diagnostic_code, - const std::string& message) { - if (text.empty()) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "A numeric field cannot be empty."); - } - char* end = nullptr; - errno = 0; - const double parsed = std::strtod(text.c_str(), &end); - if (end != text.c_str() + text.size()) { - return InputFailure("invalid-numeric-value", location, keyword, text, - "The field must use floating-point syntax."); - } - if (errno == ERANGE || !std::isfinite(parsed)) { - return ModelFailure(diagnostic_code, location, keyword, text, message); - } - value = parsed; - return true; - } - bool ContainsName(const std::vector& values, const std::string& name) const { return std::any_of(values.begin(), values.end(), @@ -603,14 +336,6 @@ class DomainMappingContext::Impl { }); } - bool ContainsName(const std::vector& values, - const std::string& name) const { - return std::any_of(values.begin(), values.end(), - [&name](const RawMaterial& value) { - return AsciiCaseInsensitiveEquals(value.name, name); - }); - } - bool ContainsSetName(const std::vector& sets, const std::string& name) const { const auto matches = [&name](const RawSet& set) { @@ -620,36 +345,14 @@ class DomainMappingContext::Impl { } bool ContainsAssemblySetName(bool node_set, const std::string& name) const { - return std::any_of(assembly_sets_.begin(), assembly_sets_.end(), + return std::any_of(topology_.AssemblySets().begin(), + topology_.AssemblySets().end(), [node_set, &name](const RawAssemblySet& set) { return set.is_node_set == node_set && AsciiCaseInsensitiveEquals(set.name, name); }); } - bool ContainsSurfaceName(const std::string& name) const { - return std::any_of(element_surfaces_.begin(), element_surfaces_.end(), - [&name](const RawElementSurface& surface) { - return AsciiCaseInsensitiveEquals(surface.name, name); - }); - } - - bool IsPressureNameToken(const std::string_view text) const noexcept { - if (text.empty() || text.size() > 80U) { - return false; - } - const auto is_initial = [](const char value) { - return (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || - value == '_'; - }; - const auto is_subsequent = [is_initial](const char value) { - return is_initial(value) || (value >= '0' && value <= '9') || - value == '-' || value == '_'; - }; - return is_initial(text.front()) && - std::all_of(text.begin() + 1, text.end(), is_subsequent); - } - void ParseBlocks() { definition_.source_path = input_.source_path; definition_.source_content_identity = input_.source_content_identity; @@ -663,15 +366,15 @@ class DomainMappingContext::Impl { ParsePartBlock(block); } else if (in_assembly_) { ParseAssemblyBlock(block); - } else if (in_step_) { - ParseStepBlock(block); + } else if (steps_.InStep()) { + steps_.ParseStepBlock(block); } else { ParseTopLevelBlock(block, index); } } if (!failure_ && - (current_part_ || in_assembly_ || in_instance_ || in_step_)) { + (current_part_ || in_assembly_ || in_instance_ || steps_.InStep())) { const auto location = input_.blocks.empty() ? SourceLocation{input_.source_path, 0U} : input_.blocks.back().location; @@ -681,9 +384,10 @@ class DomainMappingContext::Impl { } void ParseTopLevelBlock(const KeywordBlock& block, std::size_t index) { - if (step_seen_) { + if (steps_.HasStep()) { if (block.canonical_name == "STEP") { - ParseStepStart(block); + steps_.ParseStepStart(block, + assembly_seen_ && !material_properties_.Empty()); } else { InvalidKeywordLocation( block, "The sole analysis step must be the final top-level block."); @@ -695,10 +399,10 @@ class DomainMappingContext::Impl { block.canonical_name != "DENSITY" && block.canonical_name != "CONDUCTIVITY" && block.canonical_name != "SPECIFIC HEAT") { - material_eligible_.reset(); + material_properties_.EndMaterial(); } - pending_section_.reset(); - active_output_ = false; + material_properties_.EndSection(); + steps_.EndOutput(); if (block.canonical_name == "HEADING") { if (index != 0U || heading_seen_ || !ValidateParameters(block, {})) { @@ -728,7 +432,7 @@ class DomainMappingContext::Impl { if (!RequireNoData(block)) { return; } - AddIgnoredWarning(block); + steps_.AddIgnoredWarning(block); return; } if (block.canonical_name == "PHYSICAL CONSTANTS") { @@ -743,7 +447,7 @@ class DomainMappingContext::Impl { block.canonical_name)) { if (!physical_constants_location_) physical_constants_location_ = block.location; - AddIgnoredWarning(block); + steps_.AddIgnoredWarning(block); } return; } @@ -756,53 +460,56 @@ class DomainMappingContext::Impl { return; } if (block.canonical_name == "AMPLITUDE") { - ParseAmplitude(block); + steps_.ParseAmplitude(block); return; } if (block.canonical_name == "MATERIAL") { - ParseMaterial(block); + if (!assembly_seen_ || model_boundary_seen_) { + InvalidKeywordLocation(block, + "Material definitions follow the assembly and " + "precede model boundaries."); + return; + } + material_properties_.ParseMaterial(block); return; } if (block.canonical_name == "ELASTIC") { - ParseElastic(block); + material_properties_.ParseElastic(block); return; } if (block.canonical_name == "DENSITY") { - if (!material_eligible_) { - RejectUnknown(block); + if (!material_properties_.HasActiveMaterial()) { + RejectUnknown(block, model_element_family_ == ElementFamily::kShell); return; } - ParseDensity(block); + material_properties_.ParseDensity(block); return; } if (block.canonical_name == "CONDUCTIVITY") { - ParseThermalProperty(block, true); + material_properties_.ParseThermalProperty(block, true); return; } if (block.canonical_name == "SPECIFIC HEAT") { - ParseThermalProperty(block, false); + material_properties_.ParseThermalProperty(block, false); return; } if (block.canonical_name == "INITIAL CONDITIONS") { - ParseInitialConditions(block); + steps_.ParseInitialConditions(block); return; } if (block.canonical_name == "BOUNDARY") { - if (!assembly_seen_ || materials_.empty()) { + if (!assembly_seen_ || material_properties_.Empty()) { InvalidKeywordLocation( block, "Model boundary data follows the assembly and materials."); return; } model_boundary_seen_ = true; - if (model_element_family_ == ElementFamily::kThermalShell) { - ParseThermalBoundary(block, thermal_model_boundaries_); - } else { - ParseBoundary(block, model_boundaries_); - } + steps_.ParseModelBoundary(block); return; } if (block.canonical_name == "STEP") { - ParseStepStart(block); + steps_.ParseStepStart(block, + assembly_seen_ && !material_properties_.Empty()); return; } if (block.canonical_name == "END PART" || @@ -814,11 +521,12 @@ class DomainMappingContext::Impl { "The closing keyword has no matching open block."); return; } - RejectUnknown(block); + RejectUnknown(block, model_element_family_ == ElementFamily::kShell); } void ParsePartStart(const KeywordBlock& block) { - if (assembly_seen_ || !materials_.empty() || model_boundary_seen_) { + if (assembly_seen_ || !material_properties_.Empty() || + model_boundary_seen_) { InvalidKeywordLocation( block, "All part blocks must precede the sole assembly block."); return; @@ -830,14 +538,14 @@ class DomainMappingContext::Impl { if (name == nullptr) { return; } - if (ContainsName(parts_, *name)) { + if (ContainsName(topology_.Parts(), *name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Part names are unique under case-insensitive lookup."); return; } - parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}}); - current_part_ = parts_.size() - 1U; + topology_.Parts().push_back({*name, block.location, {}, {}, {}, {}, {}}); + current_part_ = topology_.Parts().size() - 1U; part_elements_seen_ = false; part_sets_seen_ = false; part_sections_seen_ = false; @@ -849,20 +557,20 @@ class DomainMappingContext::Impl { if (!ValidateParameters(block, {}) || !RequireNoData(block)) { return; } - const RawPart& part = parts_[*current_part_]; + const RawPart& part = topology_.Parts()[*current_part_]; const bool shell_part = model_element_family_ == ElementFamily::kShell || model_element_family_ == ElementFamily::kThermalShell; if (part.nodes.empty() || part.elements.empty() || (!shell_part && model_element_family_ != ElementFamily::kSolid && - part.sections.empty())) { + part.properties.sections.empty())) { InvalidKeywordLocation(block, "A part closes only after node, element, " "and matching section blocks."); return; } current_part_.reset(); - pending_section_.reset(); + material_properties_.EndSection(); beam_section_context_active_ = false; return; } @@ -879,7 +587,7 @@ class DomainMappingContext::Impl { return; } - RawPart& part = parts_[*current_part_]; + RawPart& part = topology_.Parts()[*current_part_]; if (block.canonical_name == "NODE") { beam_section_context_active_ = false; if (part_elements_seen_ || part_sets_seen_ || part_sections_seen_) { @@ -888,7 +596,7 @@ class DomainMappingContext::Impl { "NODE blocks must precede element, set, and section blocks."); return; } - pending_section_.reset(); + material_properties_.EndSection(); ParseNodes(block, part); } else if (block.canonical_name == "ELEMENT") { beam_section_context_active_ = false; @@ -898,7 +606,7 @@ class DomainMappingContext::Impl { "and precede sets and sections."); return; } - pending_section_.reset(); + material_properties_.EndSection(); ParseElements(block, part); part_elements_seen_ = !failure_; } else if (block.canonical_name == "NSET") { @@ -909,7 +617,7 @@ class DomainMappingContext::Impl { "Part sets follow element blocks and precede beam sections."); return; } - pending_section_.reset(); + material_properties_.EndSection(); ParseSet(block, true, part); part_sets_seen_ = !failure_; } else if (block.canonical_name == "ELSET") { @@ -920,7 +628,7 @@ class DomainMappingContext::Impl { "Part sets follow element blocks and precede beam sections."); return; } - pending_section_.reset(); + material_properties_.EndSection(); ParseSet(block, false, part); part_sets_seen_ = !failure_; } else if (block.canonical_name == "BEAM GENERAL SECTION") { @@ -939,7 +647,7 @@ class DomainMappingContext::Impl { "Beam-section semantics cannot be mixed with shell elements."); return; } - ParseBeamSection(block, part); + material_properties_.ParseBeamSection(block, part.properties); part_sections_seen_ = !failure_; beam_section_context_active_ = !failure_; } else if (block.canonical_name == "SHELL SECTION") { @@ -957,7 +665,7 @@ class DomainMappingContext::Impl { "Shell-section semantics cannot be mixed with beam elements."); return; } - ParseShellSection(block, part); + material_properties_.ParseShellSection(block, part.properties); part_sections_seen_ = !failure_; } else if (block.canonical_name == "SOLID SECTION") { beam_section_context_active_ = false; @@ -968,12 +676,12 @@ class DomainMappingContext::Impl { "SOLID SECTION requires an exclusive C3D8 mesh."); return; } - ParseSolidSection(block, part); + material_properties_.ParseSolidSection(block, part.properties); part_sections_seen_ = !failure_; } else if (block.canonical_name == "SECTION POINTS") { - ParseSectionPoints(block, part); + material_properties_.ParseSectionPoints(block, part.properties); } else if (block.canonical_name == "TRANSVERSE SHEAR STIFFNESS") { - pending_section_.reset(); + material_properties_.EndSection(); if (!beam_section_context_active_) { InputFailure( "invalid-keyword-location", block.location, block.canonical_name, @@ -981,15 +689,15 @@ class DomainMappingContext::Impl { "The ignored shear keyword still requires beam-section context."); return; } - AddIgnoredWarning(block); + steps_.AddIgnoredWarning(block); } else if (block.canonical_name == "SURFACE") { InputFailure("unsupported-surface-scope", block.location, block.canonical_name, "", "Pressure surfaces are supported only at assembly scope."); } else { - pending_section_.reset(); + material_properties_.EndSection(); beam_section_context_active_ = false; - RejectUnknown(block); + RejectUnknown(block, model_element_family_ == ElementFamily::kShell); } } @@ -1172,168 +880,6 @@ class DomainMappingContext::Impl { if (inline_set) part.element_sets.push_back(std::move(*inline_set)); } - /// @brief Parses material-only Solid assignment without thickness defaults. - void ParseSolidSection(const KeywordBlock& block, RawPart& part) { - if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) return; - const auto* elset = RequiredParameterValue(block, "ELSET"); - const auto* material = RequiredParameterValue(block, "MATERIAL"); - if (!elset || !material) return; - if (block.data.size() > 1U || - (!block.data.empty() && - std::any_of(block.data[0].fields.begin(), block.data[0].fields.end(), - [](const auto& field) { return !field.empty(); }))) { - InputFailure("invalid-solid-section", block.location, - block.canonical_name, *elset, - "SOLID SECTION permits omitted or blank data only."); - return; - } - part.solid_sections.push_back({*elset, *material, block.location}); - } - - /// @brief Validates and owns one model-level supported load amplitude. - void ParseAmplitude(const KeywordBlock& block) { - const bool dynamic_source = std::any_of( - input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) { - return candidate.canonical_name == "DYNAMIC"; - }); - if (model_element_family_ != ElementFamily::kSolid || !dynamic_source) { - InputFailure("unsupported-load-amplitude", block.location, - block.canonical_name, "", - "Amplitude is supported only in Solid dynamic models."); - return; - } - if (!ValidateParameters(block, {"NAME", "DEFINITION", "TIME", "VALUE"})) - return; - const auto* name = RequiredParameterValue(block, "NAME"); - if (!name) return; - for (const auto& option : - std::array, 3>{ - {{"DEFINITION", "TABULAR"}, - {"TIME", "STEP TIME"}, - {"VALUE", "RELATIVE"}}}) { - const auto* parameter = Parameter(block, option.first); - if (parameter && - (!parameter->value || - !AsciiCaseInsensitiveEquals(*parameter->value, option.second))) { - InputFailure("unsupported-amplitude-option", block.location, - block.canonical_name, option.first, - "Only TABULAR STEP TIME RELATIVE amplitude is supported."); - return; - } - } - const std::string canonical = CanonicalizeAscii(*name); - if (std::any_of(definition_.amplitudes.begin(), - definition_.amplitudes.end(), [&canonical](const auto& a) { - return a.name_canonical == canonical; - })) { - InputFailure("duplicate-amplitude", block.location, block.canonical_name, - *name, - "Amplitude names must be unique ignoring ASCII case."); - return; - } - TabularAmplitudeDefinition amplitude{*name, canonical, block.location, {}}; - for (const auto& row : block.data) { - if (row.fields.empty() || row.fields.size() % 2U != 0U) { - InputFailure("invalid-amplitude-data", row.location, - block.canonical_name, *name, - "Amplitude rows require complete time/value pairs."); - return; - } - for (std::size_t field = 0U; field < row.fields.size(); field += 2U) { - double time = 0.0; - double value = 0.0; - if (!ParseDouble(row.fields[field], time, row.location, - block.canonical_name) || - !ParseDouble(row.fields[field + 1U], value, row.location, - block.canonical_name)) - return; - if (time < 0.0 || (!amplitude.points.empty() && - !(time > amplitude.points.back()[0]))) { - InputFailure( - "invalid-amplitude-data", row.location, block.canonical_name, - *name, - "Amplitude times must be nonnegative and strictly increasing."); - return; - } - amplitude.points.push_back({time, value}); - } - } - if (amplitude.points.empty()) { - InputFailure("invalid-amplitude-data", block.location, - block.canonical_name, *name, - "Amplitude requires at least one finite time/value pair."); - return; - } - definition_.amplitudes.push_back(std::move(amplitude)); - } - - void ParseShellSection(const KeywordBlock& block, RawPart& part) { - for (const auto& candidate : block.parameters) { - if (candidate.name != "ELSET" && candidate.name != "MATERIAL") { - InputFailure("unsupported-shell-section-option", block.location, - block.canonical_name, candidate.name, - "The shell-section option is outside the centered " - "single-layer subset."); - return; - } - } - if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) { - return; - } - const auto* element_set = RequiredParameterValue(block, "ELSET"); - const auto* material = RequiredParameterValue(block, "MATERIAL"); - if (element_set == nullptr || material == nullptr) { - return; - } - if (block.data.size() != 1U || block.data[0].fields.empty() || - block.data[0].fields.size() > 2U) { - InputFailure("unsupported-shell-section-option", block.location, - block.canonical_name, *element_set, - "SHELL SECTION requires one thickness row with one optional " - "integration-point field."); - return; - } - double thickness = 0.0; - const bool parsed_thickness = - model_element_family_ == ElementFamily::kThermalShell - ? ParseThermalDouble(block.data[0].fields[0], thickness, - block.data[0].location, block.canonical_name) - : ParseModelDouble(block.data[0].fields[0], thickness, - block.data[0].location, block.canonical_name, - "invalid-shell-thickness", - "Shell thickness must be finite and positive."); - if (!parsed_thickness) { - return; - } - if (!(thickness > 0.0)) { - ModelFailure("invalid-shell-thickness", block.data[0].location, - block.canonical_name, *element_set, - "Shell thickness must be finite and positive."); - return; - } - if (block.data[0].fields.size() == 2U) { - const auto integration_points = - ParsePositiveSourceLabel(block.data[0].fields[1]); - if (!integration_points.HasValue()) { - InputFailure("unsupported-shell-section-option", block.data[0].location, - block.canonical_name, block.data[0].fields[1], - "The optional shell integration-point field must be a " - "positive integer."); - return; - } - if (model_element_family_ == ElementFamily::kThermalShell && - integration_points.Value() != 5) { - InputFailure( - "unsupported-shell-section-option", block.data[0].location, - block.canonical_name, block.data[0].fields[1], - "Thermal DS4 sections require exactly five integration points."); - return; - } - } - part.shell_sections.push_back( - {*element_set, *material, thickness, block.location}); - } - bool ParseSetMembers(const KeywordBlock& block, bool generate, std::vector& members) { if (block.data.empty()) { @@ -1447,100 +993,6 @@ class DomainMappingContext::Impl { } } - void ParseBeamSection(const KeywordBlock& block, RawPart& part) { - pending_section_.reset(); - if (!ValidateParameters(block, {"ELSET", "MATERIAL", "SECTION"})) { - return; - } - const auto* element_set = RequiredParameterValue(block, "ELSET"); - const auto* material = RequiredParameterValue(block, "MATERIAL"); - const auto* section = RequiredParameterValue(block, "SECTION"); - if (element_set == nullptr || material == nullptr || section == nullptr) { - return; - } - if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) { - InputFailure("unsupported-section-formulation", block.location, - block.canonical_name, *section, - "Only SECTION=GENERAL belongs to the approved subset."); - return; - } - if (std::any_of(part.sections.begin(), part.sections.end(), - [element_set](const RawSection& existing) { - return AsciiCaseInsensitiveEquals( - existing.element_set_name, *element_set); - })) { - InputFailure("duplicate-entity", block.location, block.canonical_name, - *element_set, - "An element set can receive only one beam section."); - return; - } - if (block.data.size() != 2U || block.data[0].fields.size() != 5U || - block.data[1].fields.size() != 3U) { - InputFailure("invalid-data-arity", block.location, block.canonical_name, - *element_set, - "A general section requires one five-field property row and " - "one three-field axis row."); - return; - } - RawSection raw{*element_set, *material, {}, {}, {}, block.location}; - for (std::size_t property = 0U; property < 5U; ++property) { - if (!ParseModelDouble(block.data[0].fields[property], - raw.properties[property], block.data[0].location, - block.canonical_name, "invalid-beam-property", - "Beam section properties must be finite.")) { - return; - } - } - for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { - if (!ParseModelDouble(block.data[1].fields[coordinate], - raw.first_axis[coordinate], block.data[1].location, - block.canonical_name, "invalid-beam-guide-vector", - "The beam guide vector must be finite.")) { - return; - } - } - part.sections.push_back(std::move(raw)); - pending_section_ = part.sections.size() - 1U; - } - - void ParseSectionPoints(const KeywordBlock& block, RawPart& part) { - if (!pending_section_ || !ValidateParameters(block, {}) || - block.data.empty()) { - if (!failure_) { - InputFailure( - "invalid-keyword-location", block.location, block.canonical_name, - "", "SECTION POINTS must immediately follow a general section."); - } - return; - } - RawSection& section = part.sections[*pending_section_]; - for (const auto& row : block.data) { - if (row.fields.size() != 2U) { - InputFailure("invalid-data-arity", row.location, block.canonical_name, - section.element_set_name, - "Section-point rows require x1 and x2."); - return; - } - std::array point{}; - if (!ParseDouble(row.fields[0], point[0], row.location, - block.canonical_name) || - !ParseDouble(row.fields[1], point[1], row.location, - block.canonical_name)) { - return; - } - if (std::find(section.section_points.begin(), - section.section_points.end(), - point) != section.section_points.end()) { - InputFailure("duplicate-entity", row.location, block.canonical_name, - section.element_set_name, - "Section points must be unique within a section."); - return; - } - section.section_points.push_back(point); - } - pending_section_.reset(); - } - void ParseAssemblyStart(const KeywordBlock& block) { if (assembly_seen_) { InputFailure("unsupported-nested-assembly", block.location, @@ -1548,7 +1000,8 @@ class DomainMappingContext::Impl { "V0 accepts exactly one non-nested assembly."); return; } - if (parts_.empty() || !materials_.empty() || model_boundary_seen_) { + if (topology_.Parts().empty() || !material_properties_.Empty() || + model_boundary_seen_) { InvalidKeywordLocation( block, "The sole assembly follows all part blocks and precedes model data."); @@ -1570,7 +1023,7 @@ class DomainMappingContext::Impl { if (!ValidateParameters(block, {}) || !RequireNoData(block)) { return; } - if (instances_.empty()) { + if (topology_.Instances().empty()) { InvalidKeywordLocation( block, "The assembly requires at least one identity instance."); return; @@ -1590,7 +1043,7 @@ class DomainMappingContext::Impl { } ParseInstanceStart(block); } else if (block.canonical_name == "NSET") { - if (instances_.empty()) { + if (topology_.Instances().empty()) { InvalidKeywordLocation( block, "Assembly sets follow at least one identity instance."); return; @@ -1598,7 +1051,7 @@ class DomainMappingContext::Impl { assembly_set_seen_ = true; ParseAssemblySet(block, true); } else if (block.canonical_name == "ELSET") { - if (instances_.empty()) { + if (topology_.Instances().empty()) { InvalidKeywordLocation( block, "Assembly sets follow at least one identity instance."); return; @@ -1606,14 +1059,16 @@ class DomainMappingContext::Impl { assembly_set_seen_ = true; ParseAssemblySet(block, false); } else if (block.canonical_name == "SURFACE") { - if (instances_.empty()) { + if (topology_.Instances().empty()) { InvalidKeywordLocation( block, "Assembly surfaces follow at least one identity instance."); return; } - ParseElementSurface(block); + surfaces_.ParseElementSurface(block); + if (!failure_ && model_element_family_ == ElementFamily::kThermalShell) + steps_.AddIgnoredWarning(block); } else { - RejectUnknown(block); + RejectUnknown(block, model_element_family_ == ElementFamily::kShell); } } @@ -1640,12 +1095,12 @@ class DomainMappingContext::Impl { "Instance translation or rotation data is unsupported."); return; } - if (ContainsName(instances_, *name)) { + if (ContainsName(topology_.Instances(), *name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Instance names are globally unique."); return; } - instances_.push_back({*name, *part, block.location}); + topology_.Instances().push_back({*name, *part, block.location}); in_instance_ = true; } @@ -1665,12 +1120,12 @@ class DomainMappingContext::Impl { } if (block.canonical_name == "SURFACE") { InputFailure("unsupported-surface-scope", block.location, - block.canonical_name, instances_.back().name, + block.canonical_name, topology_.Instances().back().name, "Pressure surfaces are unsupported inside an instance."); return; } InputFailure("unsupported-instance-mesh-semantics", block.location, - block.canonical_name, instances_.back().name, + block.canonical_name, topology_.Instances().back().name, "Instance-local mesh definitions are unsupported."); } @@ -1744,1519 +1199,13 @@ class DomainMappingContext::Impl { if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) { return; } - assembly_sets_.push_back(std::move(set)); - } - - void ParseElementSurface(const KeywordBlock& block) { - if (!ValidateParameters(block, {"TYPE", "NAME"}, "unsupported-parameter")) { - return; - } - const auto* type = RequiredParameterValue(block, "TYPE"); - const auto* name = RequiredParameterValue(block, "NAME"); - if (type == nullptr || name == nullptr) { - return; - } - if (!IsPressureNameToken(*name)) { - InputFailure("invalid-surface-name", block.location, block.canonical_name, - *name, - "Surface names must use the approved ASCII name syntax."); - return; - } - if (!AsciiCaseInsensitiveEquals(*type, "ELEMENT")) { - InputFailure("unsupported-surface-type", block.location, - block.canonical_name, *type, - "Only TYPE=ELEMENT belongs to the approved surface subset."); - return; - } - if (ContainsSurfaceName(*name)) { - InputFailure("duplicate-entity", block.location, block.canonical_name, - *name, - "Surface names are unique in the assembly namespace."); - return; - } - if (block.data.empty()) { - InputFailure("empty-surface", block.location, block.canonical_name, *name, - "An element surface requires at least one member."); - return; - } - - RawElementSurface surface{ - *name, block.location, element_surfaces_.size(), {}}; - for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) { - const auto& row = block.data[ordinal]; - if (row.fields.size() != 2U || row.fields[0].empty() || - row.fields[1].empty()) { - InputFailure("invalid-data-arity", row.location, block.canonical_name, - *name, - "Surface rows require one member and SPOS or SNEG."); - return; - } - ShellSurfaceSide side = ShellSurfaceSide::kSpos; - if (AsciiCaseInsensitiveEquals(row.fields[1], "SPOS")) { - side = ShellSurfaceSide::kSpos; - } else if (AsciiCaseInsensitiveEquals(row.fields[1], "SNEG")) { - side = ShellSurfaceSide::kSneg; - } else { - InputFailure("unsupported-surface-side", row.location, - block.canonical_name, row.fields[1], - "Surface sides must be SPOS or SNEG."); - return; - } - surface.members.push_back({row.fields[0], side, row.location, ordinal}); - } - element_surfaces_.push_back(std::move(surface)); - if (model_element_family_ == ElementFamily::kThermalShell) - AddIgnoredWarning(block); - } - - void ParseMaterial(const KeywordBlock& block) { - if (!assembly_seen_ || model_boundary_seen_) { - InvalidKeywordLocation(block, - "Material definitions follow the assembly and " - "precede model boundaries."); - return; - } - if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) { - return; - } - const auto* name = RequiredParameterValue(block, "NAME"); - if (name == nullptr) { - return; - } - if (ContainsName(materials_, *name)) { - InputFailure("duplicate-entity", block.location, block.canonical_name, - *name, "Material names are globally unique."); - return; - } - materials_.push_back({*name, - 0.0, - 0.0, - false, - std::nullopt, - std::nullopt, - std::nullopt, - block.location, - {}, - {}}); - material_eligible_ = materials_.size() - 1U; - } - - void ParseElastic(const KeywordBlock& block) { - if (model_element_family_ == ElementFamily::kThermalShell) { - InputFailure("unsupported-keyword", block.location, block.canonical_name, - "", "ELASTIC is outside the thermal material subset."); - return; - } - if (!material_eligible_) { - InputFailure("invalid-keyword-location", block.location, - block.canonical_name, "", - "ELASTIC must immediately follow MATERIAL."); - return; - } - if (!ValidateParameters(block, {}) || block.data.size() != 1U || - block.data[0].fields.size() != 2U) { - if (!failure_) { - InputFailure("invalid-data-arity", block.location, block.canonical_name, - materials_[*material_eligible_].name, - "ELASTIC requires exactly one E, nu row."); - } - return; - } - RawMaterial& material = materials_[*material_eligible_]; - if (material.has_elastic) { - InputFailure("duplicate-entity", block.location, block.canonical_name, - material.name, "A material accepts one ELASTIC definition."); - return; - } - if (!ParseModelDouble(block.data[0].fields[0], material.youngs_modulus, - block.data[0].location, block.canonical_name, - model_element_family_ == ElementFamily::kSolid - ? "invalid-solid-material" - : model_element_family_ == ElementFamily::kShell - ? "invalid-shell-material" - : "invalid-beam-property", - "Elastic material values must be finite.") || - !ParseModelDouble(block.data[0].fields[1], material.poisson_ratio, - block.data[0].location, block.canonical_name, - model_element_family_ == ElementFamily::kSolid - ? "invalid-solid-material" - : model_element_family_ == ElementFamily::kShell - ? "invalid-shell-material" - : "invalid-beam-property", - "Elastic material values must be finite.")) { - return; - } - material.has_elastic = true; - material.elastic_location = block.data[0].location; - } - - bool ParseDensityValue(const std::string& text, double& value, - const SourceLocation& location) { - const std::string canonical = CanonicalizeAscii(text); - if (canonical == "NAN" || canonical == "+NAN" || canonical == "-NAN" || - canonical == "INF" || canonical == "+INF" || canonical == "-INF" || - canonical == "INFINITY" || canonical == "+INFINITY" || - canonical == "-INFINITY") { - return ModelFailure( - "invalid-material-density", location, "DENSITY", text, - "Density must convert to a finite value greater than zero."); - } - std::size_t cursor = 0U; - if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { - ++cursor; - } - const std::size_t integer_start = cursor; - while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { - ++cursor; - } - const bool has_integer_digits = cursor != integer_start; - bool has_fraction_digits = false; - if (cursor < text.size() && text[cursor] == '.') { - ++cursor; - const std::size_t fraction_start = cursor; - while (cursor < text.size() && text[cursor] >= '0' && - text[cursor] <= '9') { - ++cursor; - } - has_fraction_digits = cursor != fraction_start; - } - if (!has_integer_digits && !has_fraction_digits) { - return InputFailure("invalid-density-data", location, "DENSITY", text, - "Density requires C-locale decimal syntax."); - } - if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) { - ++cursor; - if (cursor < text.size() && - (text[cursor] == '+' || text[cursor] == '-')) { - ++cursor; - } - const std::size_t exponent_start = cursor; - while (cursor < text.size() && text[cursor] >= '0' && - text[cursor] <= '9') { - ++cursor; - } - if (cursor == exponent_start) { - return InputFailure("invalid-density-data", location, "DENSITY", text, - "Density exponents require one or more digits."); - } - } - if (cursor != text.size()) { - return InputFailure("invalid-density-data", location, "DENSITY", text, - "Density requires C-locale decimal syntax."); - } - const char* begin = text.data(); - if (*begin == '+') { - ++begin; - } - double parsed = 0.0; - const auto conversion = std::from_chars(begin, text.data() + text.size(), - parsed, std::chars_format::general); - if (conversion.ec != std::errc{} || - conversion.ptr != text.data() + text.size() || !std::isfinite(parsed) || - !(parsed > 0.0)) { - return ModelFailure( - "invalid-material-density", location, "DENSITY", text, - "Density must convert to a finite value greater than zero."); - } - value = parsed; - return true; - } - - void ParseDensity(const KeywordBlock& block) { - if (!material_eligible_) { - InputFailure("invalid-density-data", block.location, block.canonical_name, - "", "DENSITY must be declared inside one MATERIAL."); - return; - } - if (!ValidateParameters(block, {}, "unsupported-density-option")) { - return; - } - RawMaterial& material = materials_[*material_eligible_]; - if (material.density) { - InputFailure("duplicate-density", block.location, block.canonical_name, - material.name, "A material accepts one DENSITY definition."); - return; - } - if (block.data.size() != 1U || - (block.data[0].fields.size() != 1U && - (block.data[0].fields.size() != 2U || - !block.data[0].fields[1].empty())) || - block.data[0].fields[0].empty()) { - InputFailure("invalid-density-data", block.location, block.canonical_name, - material.name, "DENSITY requires exactly rho or rho,."); - return; - } - double rho = 0.0; - if (!ParseDensityValue(block.data[0].fields[0], rho, - block.data[0].location)) { - return; - } - material.density = rho; - material.density_location = block.data[0].location; - } - - void ParseThermalProperty(const KeywordBlock& block, bool conductivity) { - if (model_element_family_ != ElementFamily::kThermalShell || - !material_eligible_ || !ValidateParameters(block, {})) { - if (!failure_) { - InputFailure("invalid-keyword-location", block.location, - block.canonical_name, "", - "Thermal properties belong to a DS4 material."); - } - return; - } - RawMaterial& material = materials_[*material_eligible_]; - std::optional& destination = - conductivity ? material.conductivity : material.specific_heat; - if (destination || block.data.size() != 1U || - (block.data[0].fields.size() != 1U && - (block.data[0].fields.size() != 2U || - !block.data[0].fields[1].empty()))) { - InputFailure("invalid-data-arity", block.location, block.canonical_name, - material.name, - "Thermal property requires one constant row."); - return; - } - double value = 0.0; - if (!ParseThermalDouble(block.data[0].fields[0], value, - block.data[0].location, block.canonical_name) || - !(value > 0.0)) { - if (!failure_) { - ModelFailure("invalid-thermal-material", block.data[0].location, - block.canonical_name, material.name, - "Thermal material values must be finite and positive."); - } - return; - } - destination = value; - } - - void ParseInitialConditions(const KeywordBlock& block) { - if (model_element_family_ != ElementFamily::kThermalShell || - !ValidateParameters(block, {"TYPE"}) || block.data.empty()) { - if (!failure_) { - InputFailure("invalid-data-arity", block.location, block.canonical_name, - "", "Thermal initial conditions require data rows."); - } - return; - } - const auto* type = RequiredParameterValue(block, "TYPE"); - if (type == nullptr || !AsciiCaseInsensitiveEquals(*type, "TEMPERATURE")) { - if (!failure_) - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "TYPE", - "Only TYPE=TEMPERATURE is supported."); - return; - } - for (const auto& row : block.data) { - if (row.fields.size() != 2U || row.fields[0].empty()) { - InputFailure("invalid-data-arity", row.location, block.canonical_name, - "", "Initial temperatures require target, value."); - return; - } - double value = 0.0; - if (!ParseThermalDouble(row.fields[1], value, row.location, - block.canonical_name)) - return; - thermal_initial_conditions_.push_back( - {row.fields[0], value, row.location}); - } - } - - void ParseThermalBoundary(const KeywordBlock& block, - std::vector& destination) { - if (!ValidateParameters(block, {}) || block.data.empty()) { - if (!failure_) - InputFailure("invalid-data-arity", block.location, block.canonical_name, - "", "Thermal BOUNDARY requires data rows."); - return; - } - for (const auto& row : block.data) { - if ((row.fields.size() != 3U && row.fields.size() != 4U) || - row.fields[0].empty()) { - InputFailure("invalid-data-arity", row.location, block.canonical_name, - "", "Thermal boundaries require target, DOF, DOF, value."); - return; - } - std::int64_t first = 0; - std::int64_t last = 0; - if (!ParseInteger(row.fields[1], first, row.location, - block.canonical_name, true) || - !ParseInteger(row.fields[2], last, row.location, block.canonical_name, - true) || - first != last || (first != 11 && first != 15)) { - InputFailure("invalid-dof", row.location, block.canonical_name, - row.fields[0], "Thermal boundaries support DOF 11 or 15."); - return; - } - double value = 0.0; - if (row.fields.size() == 4U && - !ParseThermalDouble(row.fields[3], value, row.location, - block.canonical_name)) - return; - destination.push_back( - {row.fields[0], static_cast(first), value, row.location}); - } - } - - void ParseBoundary( - const KeywordBlock& block, - std::vector& destination) { - const bool dynamic_input = std::any_of( - input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) { - return candidate.canonical_name == "DYNAMIC"; - }); - if (!ValidateParameters(block, {}, - dynamic_input ? "unsupported-dynamic-boundary" - : "invalid-keyword-parameter") || - block.data.empty()) { - if (!failure_) { - InputFailure("invalid-data-arity", block.location, block.canonical_name, - "", "BOUNDARY requires one or more data rows."); - } - return; - } - for (const auto& row : block.data) { - if (row.fields.size() != 3U && row.fields.size() != 4U) { - InputFailure("invalid-data-arity", row.location, block.canonical_name, - "", - "BOUNDARY rows require target, first DOF, last DOF, and " - "optional value."); - return; - } - if (row.fields[0].empty()) { - InputFailure("unresolved-reference", row.location, block.canonical_name, - "", "A boundary target cannot be empty."); - return; - } - std::int64_t first = 0; - std::int64_t last = 0; - if (!ParseInteger(row.fields[1], first, row.location, - block.canonical_name, true) || - !ParseInteger(row.fields[2], last, row.location, block.canonical_name, - true)) { - return; - } - if (first < 1 || first > 6 || last < first || last > 6) { - InputFailure("invalid-dof", row.location, block.canonical_name, - row.fields[0], - "Boundary DOFs must be an ordered range in 1..6."); - return; - } - double value = 0.0; - if (row.fields.size() == 4U && - !(model_element_family_ == ElementFamily::kSolid && - row.fields[3].empty()) && - !ParseDouble(row.fields[3], value, row.location, - block.canonical_name)) { - return; - } - destination.push_back({row.fields[0], static_cast(first), - static_cast(last), value, row.location}); - } - } - - void ParseCload(const KeywordBlock& block, std::vector& loads) { - if (step_.dynamic && model_element_family_ != ElementFamily::kSolid && - !block.parameters.empty()) { - InputFailure("unsupported-dynamic-load", block.location, - block.canonical_name, "", - "Dynamic CLOAD does not support keyword parameters."); - return; - } - const bool solid_dynamic = - step_.dynamic && model_element_family_ == ElementFamily::kSolid; - if (!ValidateParameters( - block, solid_dynamic ? std::vector{"AMPLITUDE"} - : std::vector{}) || - block.data.empty()) { - if (!failure_) { - InputFailure("invalid-data-arity", block.location, block.canonical_name, - "", "CLOAD requires one or more rows."); - } - return; - } - std::optional amplitude_index; - std::string amplitude_name; - if (Parameter(block, "AMPLITUDE")) { - const auto* name = RequiredParameterValue(block, "AMPLITUDE"); - if (!name) return; - amplitude_name = *name; - for (std::size_t index = 0U; index < definition_.amplitudes.size(); - ++index) { - if (AsciiCaseInsensitiveEquals(definition_.amplitudes[index].name_raw, - *name)) { - amplitude_index = static_cast(index); - break; - } - } - if (!amplitude_index) { - InputFailure("unresolved-amplitude", block.location, - block.canonical_name, *name, - "CLOAD amplitude must resolve to a model-level curve."); - return; - } - } - for (const auto& row : block.data) { - if (row.fields.size() != 3U || row.fields[0].empty()) { - InputFailure("invalid-data-arity", row.location, block.canonical_name, - "", "CLOAD rows require target, DOF, magnitude."); - return; - } - std::int64_t dof = 0; - double magnitude = 0.0; - if (!ParseInteger(row.fields[1], dof, row.location, block.canonical_name, - true) || - !ParseDouble(row.fields[2], magnitude, row.location, - block.canonical_name)) { - return; - } - if (dof < 1 || dof > 6) { - InputFailure("invalid-dof", row.location, block.canonical_name, - row.fields[0], "CLOAD DOF must be in 1..6."); - return; - } - if ((step_.dynamic || model_element_family_ == ElementFamily::kSolid) && - dof > 3) { - InputFailure("unsupported-dynamic-load", row.location, - block.canonical_name, row.fields[1], - "Dynamic CLOAD supports global translational DOF 1 " - "through 3 only."); - return; - } - loads.push_back({row.fields[0], static_cast(dof), magnitude, - row.location, amplitude_index, amplitude_name, - block.location}); - } - } - - void ParseDsload(const KeywordBlock& block) { - if (!ValidateParameters(block, {}, "unsupported-dsload-parameter") || - block.data.empty()) { - if (!failure_) { - InputFailure("invalid-data-arity", block.location, block.canonical_name, - "", "DSLOAD requires one or more rows."); - } - return; - } - for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) { - const auto& row = block.data[ordinal]; - if (row.fields.size() != 3U || row.fields[0].empty() || - row.fields[1].empty() || row.fields[2].empty()) { - InputFailure("invalid-data-arity", row.location, block.canonical_name, - "", "DSLOAD rows require surface, P, magnitude."); - return; - } - if (!IsPressureNameToken(row.fields[0])) { - InputFailure("invalid-surface-name", row.location, block.canonical_name, - row.fields[0], - "DSLOAD surface names must use the approved ASCII name " - "syntax."); - return; - } - if (!AsciiCaseInsensitiveEquals(row.fields[1], "P")) { - InputFailure("unsupported-load-type", row.location, - block.canonical_name, row.fields[1], - "Only uniform surface pressure load type P is supported."); - return; - } - double magnitude = 0.0; - if (!ParseSurfacePressureDouble(row.fields[2], magnitude, row.location, - block.canonical_name)) { - return; - } - step_.surface_pressure_loads.push_back( - {row.fields[0], magnitude, row.fields[2], block.location, - row.location, ordinal, step_.next_load_ordinal++}); - } - } - - void ParseStepStart(const KeywordBlock& block) { - if (step_seen_) { - if (step_.frequency || step_.dynamic) { - InputFailure( - "unsupported-analysis-procedure", block.location, - block.canonical_name, "", - "The frequency subset accepts exactly one procedure step."); - } else { - const auto* nlgeom = Parameter(block, "NLGEOM"); - const bool nonlinear_second_step = - nlgeom != nullptr && nlgeom->value && - AsciiCaseInsensitiveEquals(*nlgeom->value, "YES"); - if (step_.nlgeom || nonlinear_second_step) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, "", - "The nonlinear static subset accepts exactly one " - "procedure step."); - } else { - InputFailure("unsupported-multiple-step", block.location, - block.canonical_name, "", - "V0 accepts exactly one analysis step."); - } - } - return; - } - if (!assembly_seen_ || materials_.empty()) { - InvalidKeywordLocation(block, - "The sole step follows the complete assembly and " - "material definitions."); - return; - } - if (!ValidateParameters(block, {"NAME", "NLGEOM", "PERTURBATION", "INC"}) || - !RequireNoData(block)) { - return; - } - StaticProcedureKind procedure_kind = - StaticProcedureKind::kLinearSmallDisplacement; - bool nlgeom = false; - const auto* nlgeom_parameter = Parameter(block, "NLGEOM"); - if (nlgeom_parameter != nullptr) { - if (!nlgeom_parameter->value || nlgeom_parameter->value->empty()) { - InputFailure("invalid-keyword-parameter", block.location, - block.canonical_name, "NLGEOM", - "NLGEOM requires YES or NO in the approved subset."); - return; - } - if (AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "YES")) { - if (model_element_family_ != ElementFamily::kShell && - model_element_family_ != ElementFamily::kSolid) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, *nlgeom_parameter->value, - "NLGEOM=YES requires a supported Shell or Solid model."); - return; - } - procedure_kind = StaticProcedureKind::kNonlinearFullNewton; - nlgeom = true; - } else if (!AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "NO")) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, *nlgeom_parameter->value, - "NLGEOM must select YES or NO."); - return; - } - } - const auto* perturbation = Parameter(block, "PERTURBATION"); - if (perturbation != nullptr && perturbation->value) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, "PERTURBATION", - "PERTURBATION is accepted only as a value-less flag."); - return; - } - const auto* name = Parameter(block, "NAME"); - if (name != nullptr && (!name->value || name->value->empty())) { - InputFailure("invalid-keyword-parameter", block.location, - block.canonical_name, "NAME", - "STEP NAME must not be empty when it is present."); - return; - } - const auto* increment_limit = Parameter(block, "INC"); - std::uint32_t maximum_increments = 100U; - if (increment_limit != nullptr) { - if (!increment_limit->value || increment_limit->value->empty()) { - InputFailure("invalid-dynamic-data", block.location, - block.canonical_name, "INC", - "INC requires a positive uint32 value."); - return; - } - std::int64_t parsed_limit = 0; - if (!ParseInteger(*increment_limit->value, parsed_limit, block.location, - block.canonical_name, true) || - parsed_limit > static_cast( - std::numeric_limits::max())) { - if (!failure_) { - InputFailure("invalid-dynamic-data", block.location, - block.canonical_name, "INC", - "INC requires a positive uint32 value."); - } - return; - } - maximum_increments = static_cast(parsed_limit); - } - step_seen_ = true; - in_step_ = true; - step_ = {}; - step_.location = block.location; - step_.name_raw = name == nullptr ? "Step-1" : *name->value; - step_.name_canonical = CanonicalizeAscii(step_.name_raw); - step_.procedure_kind = procedure_kind; - step_.nlgeom = nlgeom; - step_.nlgeom_explicit = nlgeom_parameter != nullptr; - step_.perturbation_explicit = perturbation != nullptr; - step_.maximum_increments = maximum_increments; - step_load_seen_ = false; - step_no_op_seen_ = false; - } - - void ParseStepBlock(const KeywordBlock& block) { - if (model_element_family_ == ElementFamily::kSolid && - (block.canonical_name == "STATIC" || - block.canonical_name == "FREQUENCY" || - block.canonical_name == "DYNAMIC") && - (step_.has_static || step_.frequency || step_.dynamic)) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, step_.name_raw, - "Solid steps accept exactly one procedure."); - return; - } - if (model_element_family_ == ElementFamily::kThermalShell && - (block.canonical_name == "STATIC" || - block.canonical_name == "FREQUENCY" || - block.canonical_name == "DYNAMIC")) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, step_.name_raw, - "DS4 supports only one HEAT TRANSFER procedure."); - return; - } - if (block.canonical_name == "END STEP") { - active_output_ = false; - if (!ValidateParameters(block, {}) || !RequireNoData(block)) { - return; - } - if (!step_.has_static && !step_.frequency && !step_.dynamic && - !step_.has_heat_transfer) { - InvalidKeywordLocation( - block, "The sole step requires one supported procedure keyword."); - return; - } - in_step_ = false; - return; - } - if (block.canonical_name == "STEP") { - InputFailure("unsupported-multiple-step", block.location, - block.canonical_name, "", - "A second or nested step is unsupported."); - return; - } - if (block.canonical_name == "HEAT TRANSFER") { - active_output_ = false; - if (step_.has_static || step_.frequency || step_.dynamic || - step_.has_heat_transfer || !step_.boundaries.empty() || - !step_.loads.empty() || step_no_op_seen_) { - InvalidKeywordLocation(block, - "HEAT TRANSFER must be the first procedure."); - return; - } - ParseHeatTransfer(block); - } else if (block.canonical_name == "STATIC") { - active_output_ = false; - if (!step_.boundaries.empty() || !step_.loads.empty() || - !step_.surface_pressure_loads.empty() || step_no_op_seen_) { - InvalidKeywordLocation( - block, "STATIC must be the first keyword in the sole step."); - return; - } - ParseStatic(block); - } else if (block.canonical_name == "FREQUENCY") { - active_output_ = false; - if (step_.has_static || step_.frequency || !step_.boundaries.empty() || - !step_.loads.empty() || !step_.surface_pressure_loads.empty() || - step_no_op_seen_) { - InvalidKeywordLocation( - block, "FREQUENCY must be the first procedure in the sole step."); - return; - } - ParseFrequency(block); - } else if (block.canonical_name == "DYNAMIC") { - active_output_ = false; - if (step_.has_static || step_.frequency || step_.dynamic || - !step_.boundaries.empty() || !step_.loads.empty() || - !step_.surface_pressure_loads.empty() || step_no_op_seen_) { - InvalidKeywordLocation( - block, "DYNAMIC must be the first procedure in the sole step."); - return; - } - ParseDynamic(block); - } else if (block.canonical_name == "BOUNDARY") { - active_output_ = false; - if ((!step_.has_static && !step_.frequency && !step_.dynamic && - !step_.has_heat_transfer) || - step_load_seen_ || step_no_op_seen_) { - InvalidKeywordLocation( - block, - "Step boundaries follow STATIC and precede loads " - "and no-op requests."); - return; - } - if (step_.dynamic && !block.parameters.empty()) { - InputFailure("unsupported-dynamic-boundary", block.location, - block.canonical_name, "", - "Dynamic BOUNDARY does not support keyword parameters."); - return; - } - if (step_.has_heat_transfer) { - ParseThermalBoundary(block, step_.thermal_boundaries); - } else { - ParseBoundary(block, step_.boundaries); - } - if (!failure_ && step_.frequency) { - ValidateEigenBoundaries(step_.boundaries); - } - if (!failure_ && step_.dynamic) { - ValidateDynamicBoundaries(step_.boundaries); - } - } else if (block.canonical_name == "CLOAD") { - active_output_ = false; - if ((!step_.has_static && !step_.dynamic) || step_no_op_seen_) { - InvalidKeywordLocation( - block, - "CLOAD follows STATIC and boundaries and precedes no-op requests."); - return; - } - ParseCload(block, step_.loads); - if (!failure_) { - step_.next_load_ordinal += block.data.size(); - } - step_load_seen_ = !failure_; - } else if (block.canonical_name == "CFLUX") { - active_output_ = false; - if (!step_.has_heat_transfer || step_no_op_seen_) { - InvalidKeywordLocation(block, - "CFLUX follows HEAT TRANSFER and boundaries."); - return; - } - ParseCflux(block); - step_load_seen_ = !failure_; - } else if (block.canonical_name == "DSLOAD") { - active_output_ = false; - if (!step_.has_static || step_.dynamic || step_no_op_seen_) { - InvalidKeywordLocation(block, - "DSLOAD follows STATIC and boundaries and " - "precedes no-op requests."); - return; - } - ParseDsload(block); - step_load_seen_ = !failure_; - } else if (block.canonical_name == "RESTART") { - active_output_ = false; - if (!step_.has_static && !step_.frequency && !step_.dynamic && - !step_.has_heat_transfer) { - InvalidKeywordLocation( - block, "Step no-op requests follow STATIC, boundaries, and loads."); - return; - } - step_no_op_seen_ = true; - if (step_.frequency || step_.has_heat_transfer || - model_element_family_ == ElementFamily::kSolid) { - const auto* write = Parameter(block, "WRITE"); - const auto* frequency = Parameter(block, "FREQUENCY"); - if (!ValidateParameters(block, {"WRITE", "FREQUENCY"}) || - write == nullptr || write->value || frequency == nullptr || - !frequency->value || *frequency->value != "0") { - if (!failure_) { - InputFailure( - "unsupported-keyword", block.location, block.canonical_name, "", - "Supported RESTART no-op requires WRITE, FREQUENCY=0."); - } - return; - } - } - if (RequireNoData(block)) { - AddIgnoredWarning(block); - } - } else if (block.canonical_name == "OUTPUT") { - if (!step_.has_static && !step_.frequency && !step_.dynamic && - !step_.has_heat_transfer) { - InvalidKeywordLocation( - block, "Step no-op requests follow STATIC, boundaries, and loads."); - return; - } - step_no_op_seen_ = true; - ParseOutputRoot(block); - } else if (block.canonical_name == "NODE OUTPUT" || - block.canonical_name == "ELEMENT OUTPUT" || - block.canonical_name == "CONTACT OUTPUT" || - block.canonical_name == "ENERGY OUTPUT") { - ParseOutputChild(block); - } else { - active_output_ = false; - if (step_.procedure_kind == StaticProcedureKind::kNonlinearFullNewton && - !step_.has_static) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, "", - "The nonlinear step supports only the STATIC procedure."); - return; - } - RejectUnknown(block); - } - } - - bool ParseFrequencyModeCount(const std::string& text, std::int64_t& value, - const SourceLocation& location) { - if (text.empty() || - !std::all_of(text.begin(), text.end(), [](char character) { - return character >= '0' && character <= '9'; - })) { - return InputFailure( - "invalid-frequency-data", location, "FREQUENCY", text, - "The requested mode count must be positive decimal digits."); - } - const auto parsed = - std::from_chars(text.data(), text.data() + text.size(), value); - if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size() || - value <= 0) { - return InputFailure( - "invalid-frequency-data", location, "FREQUENCY", text, - "The requested mode count must be a positive int64 value."); - } - return true; - } - - void ParseFrequency(const KeywordBlock& block) { - if (step_.nlgeom || (step_.perturbation_explicit && step_.has_static) || - !ValidateParameters( - block, {"EIGENSOLVER", "SIM", "ACOUSTIC COUPLING", "NORMALIZATION"}, - "unsupported-frequency-option")) { - if (!failure_) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, "", - "FREQUENCY requires the original linear configuration."); - } - return; - } - if (block.data.size() != 1U || block.data[0].fields.empty() || - block.data[0].fields.size() > 7U) { - InputFailure("invalid-frequency-data", block.location, - block.canonical_name, "", - "FREQUENCY requires one data row with one to seven fields."); - return; - } - for (std::size_t field = 1U; field < block.data[0].fields.size(); ++field) { - if (!block.data[0].fields[field].empty()) { - InputFailure("invalid-frequency-data", block.data[0].location, - block.canonical_name, std::to_string(field + 1U), - "Only the requested mode-count field may be non-empty."); - return; - } - } - FrequencyStepDefinition frequency{step_.name_raw, - step_.name_canonical, - step_.location, - block.location, - block.data[0].location, - 0, - step_.nlgeom_explicit, - step_.perturbation_explicit, - false, - false, - false, - false, - FrequencyEigensolver::kLanczos, - FrequencyNormalization::kMass, - {}}; - if (!ParseFrequencyModeCount(block.data[0].fields[0], - frequency.requested_mode_count, - block.data[0].location)) { - return; - } - const auto* eigensolver = Parameter(block, "EIGENSOLVER"); - if (eigensolver != nullptr) { - if (!eigensolver->value || - !AsciiCaseInsensitiveEquals(*eigensolver->value, "LANCZOS")) { - InputFailure("unsupported-frequency-option", block.location, - block.canonical_name, "EIGENSOLVER", - "Only EIGENSOLVER=LANCZOS is accepted."); - return; - } - frequency.eigensolver_explicit = true; - } - const auto* sim = Parameter(block, "SIM"); - if (sim != nullptr) { - if (sim->value && !AsciiCaseInsensitiveEquals(*sim->value, "YES")) { - InputFailure("unsupported-frequency-option", block.location, - block.canonical_name, "SIM", - "SIM accepts only a flag or YES."); - return; - } - frequency.sim_explicit = true; - } - const auto* acoustic = Parameter(block, "ACOUSTIC COUPLING"); - if (acoustic != nullptr) { - if (!acoustic->value || - !AsciiCaseInsensitiveEquals(*acoustic->value, "ON")) { - InputFailure("unsupported-frequency-option", block.location, - block.canonical_name, "ACOUSTIC COUPLING", - "Only ACOUSTIC COUPLING=ON is accepted."); - return; - } - frequency.acoustic_coupling_explicit = true; - } - const auto* normalization = Parameter(block, "NORMALIZATION"); - if (normalization != nullptr) { - if (!normalization->value || - !AsciiCaseInsensitiveEquals(*normalization->value, "MASS")) { - InputFailure("unsupported-frequency-option", block.location, - block.canonical_name, "NORMALIZATION", - "Only NORMALIZATION=MASS is accepted."); - return; - } - frequency.normalization_explicit = true; - } - step_.frequency = std::move(frequency); - definition_.warnings.push_back( - {Severity::kWarning, "source-eigensolver-mapped-to-feast", - block.location, block.canonical_name, step_.name_raw, - "The accepted source eigensolver is mapped to the FEAST backend."}); - if (step_.frequency->sim_explicit) { - definition_.warnings.push_back( - {Severity::kWarning, "source-sim-option-not-applied", block.location, - block.canonical_name, step_.name_raw, - "The accepted source SIM option has no FESA runtime effect."}); - } - if (step_.frequency->acoustic_coupling_explicit) { - definition_.warnings.push_back( - {Severity::kWarning, "acoustic-option-inapplicable-structural-only", - block.location, block.canonical_name, step_.name_raw, - "Acoustic coupling is inapplicable to this structural-only model."}); - } - } - - void ValidateEigenBoundaries( - const std::vector& boundaries) { - for (const auto& boundary : boundaries) { - if (boundary.value != 0.0) { - InputFailure("unsupported-eigen-boundary", boundary.location, - "BOUNDARY", boundary.target, - "Frequency boundaries must prescribe exact zero."); - return; - } - } - } - - void ValidateDynamicBoundaries( - const std::vector& boundaries) { - for (const auto& boundary : boundaries) { - if (boundary.value != 0.0) { - InputFailure("unsupported-dynamic-boundary", boundary.location, - "BOUNDARY", boundary.target, - "Dynamic boundaries must be homogeneous and constant."); - return; - } - } - } - - void ParseDynamic(const KeywordBlock& block) { - if (step_.perturbation_explicit || - (model_element_family_ != ElementFamily::kSolid && - (!step_.nlgeom || model_element_family_ != ElementFamily::kShell)) || - !ValidateParameters(block, {"DIRECT"}, - "unsupported-dynamic-parameter")) { - if (!failure_) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, "", - "DYNAMIC requires nonlinear Shell or linear/nonlinear " - "Solid geometry."); - } - return; - } - if (model_element_family_ == ElementFamily::kSolid && - block.data.size() == 1U && block.data[0].fields.size() > 3U) { - InputFailure("invalid-dynamic-data", block.data[0].location, - block.canonical_name, "", - "Solid DYNAMIC requires h,T with at most one blank field."); - return; - } - const auto* direct = Parameter(block, "DIRECT"); - if (direct == nullptr || direct->value || block.data.size() != 1U || - block.data[0].fields.size() < 2U || - std::any_of(block.data[0].fields.begin() + 2, - block.data[0].fields.end(), - [](const auto& field) { return !field.empty(); })) { - InputFailure( - "invalid-dynamic-data", block.location, block.canonical_name, "", - "DYNAMIC,DIRECT requires one row with increment and time period."); - return; - } - DynamicStepDefinition dynamic{step_.name_raw, - step_.name_canonical, - step_.location, - block.location, - block.data[0].location, - 0.0, - 0.0, - 0U, - step_.maximum_increments, - -0.05, - 0.275625, - 0.55, - {}, - {}}; - if (!ParseDynamicDouble(block.data[0].fields[0], dynamic.initial_increment, - block.data[0].location, block.canonical_name) || - !ParseDynamicDouble(block.data[0].fields[1], dynamic.time_period, - block.data[0].location, block.canonical_name) || - !(dynamic.initial_increment > 0.0) || !(dynamic.time_period > 0.0)) { - if (!failure_) { - InputFailure( - "invalid-dynamic-data", block.data[0].location, - block.canonical_name, "", - "Dynamic increment and time period must be finite and positive."); - } - return; - } - const double count = dynamic.time_period / dynamic.initial_increment; - const double rounded_count = std::round(count); - if (!std::isfinite(count) || rounded_count < 1.0 || - rounded_count > - static_cast(std::numeric_limits::max()) || - dynamic.initial_increment * rounded_count != dynamic.time_period) { - InputFailure("invalid-dynamic-schedule", block.data[0].location, - block.canonical_name, "", - "DYNAMIC,DIRECT requires an exact positive integer number " - "of increments."); - return; - } - dynamic.increment_count = static_cast(rounded_count); - if (dynamic.increment_count > dynamic.maximum_increments) { - InputFailure("invalid-dynamic-schedule", block.data[0].location, - block.canonical_name, "", - "DYNAMIC,DIRECT increment count cannot exceed STEP INC."); - return; - } - dynamic.nlgeom = step_.nlgeom; - step_.dynamic = std::move(dynamic); - } - - void ParseHeatTransfer(const KeywordBlock& block) { - if (model_element_family_ != ElementFamily::kThermalShell || step_.nlgeom || - step_.perturbation_explicit || - !ValidateParameters(block, {"END"}, "unsupported-thermal-option") || - block.data.size() != 1U) { - if (!failure_) - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, "", - "HEAT TRANSFER requires the DS4 thermal subset."); - return; - } - const auto* end = Parameter(block, "END"); - if (end != nullptr && - (!end->value || !AsciiCaseInsensitiveEquals(*end->value, "PERIOD"))) { - InputFailure("unsupported-thermal-option", block.location, - block.canonical_name, "END", - "HEAT TRANSFER accepts only END=PERIOD."); - return; - } - if (block.data[0].fields.size() > 5U) { - InputFailure( - "invalid-thermal-time-grid", block.data[0].location, - block.canonical_name, "", - "HEAT TRANSFER permits at most three trailing empty fields."); - return; - } - const auto fields = WithoutTrailingEmpty(block.data[0].fields); - if (fields.size() != 2U) { - InputFailure("invalid-thermal-time-grid", block.data[0].location, - block.canonical_name, "", - "HEAT TRANSFER requires increment and time period."); - return; - } - if (!ParseThermalDouble(fields[0], step_.thermal_increment, - block.data[0].location, block.canonical_name) || - !ParseThermalDouble(fields[1], step_.thermal_period, - block.data[0].location, block.canonical_name) || - !(step_.thermal_increment > 0.0) || !(step_.thermal_period > 0.0)) { - if (!failure_) - InputFailure( - "invalid-thermal-time-grid", block.data[0].location, - block.canonical_name, "", - "Thermal increment and period must be finite and positive."); - return; - } - const auto grid = - ThermalTimeGrid::Create(step_.thermal_period, step_.thermal_increment, - step_.maximum_increments); - if (!grid.HasValue()) { - auto diagnostic = grid.GetStatus().Diagnostics().front(); - diagnostic.location = block.data[0].location; - diagnostic.keyword = block.canonical_name; - diagnostic.entity_identity = step_.name_raw; - failure_ = MappingFailure{ - grid.GetStatus().Category().value_or(FailureCategory::kInput), - std::move(diagnostic)}; - return; - } - step_.has_heat_transfer = true; - } - - void ParseCflux(const KeywordBlock& block) { - if (!ValidateParameters(block, {}) || block.data.empty()) { - if (!failure_) - InputFailure("invalid-data-arity", block.location, block.canonical_name, - "", "CFLUX requires target, DOF, power rows."); - return; - } - for (const auto& row : block.data) { - if (row.fields.size() != 3U || row.fields[0].empty()) { - InputFailure("invalid-data-arity", row.location, block.canonical_name, - "", "CFLUX rows require target, DOF, power."); - return; - } - std::int64_t dof = 0; - double power = 0.0; - if (!ParseInteger(row.fields[1], dof, row.location, block.canonical_name, - true) || - !ParseThermalDouble(row.fields[2], power, row.location, - block.canonical_name) || - (dof != 11 && dof != 15)) { - InputFailure("invalid-dof", row.location, block.canonical_name, - row.fields[0], "CFLUX supports DOF 11 or 15."); - return; - } - step_.thermal_fluxes.push_back( - {row.fields[0], static_cast(dof), power, row.location}); - } - } - - void ParseStatic(const KeywordBlock& block) { - if (model_element_family_ == ElementFamily::kSolid && - Parameter(block, "DIRECT")) { - if (step_.perturbation_explicit || step_.has_static || step_.frequency || - step_.dynamic) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, step_.name_raw, - "STATIC DIRECT requires one non-perturbation procedure."); - return; - } - if (!ValidateParameters(block, {"DIRECT"})) return; - const auto* direct = Parameter(block, "DIRECT"); - if (direct->value || block.data.size() != 1U || - (block.data[0].fields.size() < 2U || - block.data[0].fields.size() > 3U || - std::any_of(block.data[0].fields.begin() + 2, - block.data[0].fields.end(), - [](const auto& field) { return !field.empty(); }))) { - InputFailure("invalid-static-data", block.location, - block.canonical_name, "DIRECT", - "STATIC DIRECT requires exactly h,T."); - return; - } - double h = 0.0; - double period = 0.0; - if (!ParseDouble(block.data[0].fields[0], h, block.data[0].location, - "STATIC") || - !ParseDouble(block.data[0].fields[1], period, block.data[0].location, - "STATIC")) - return; - const double count = std::round(period / h); - if (!(h > 0.0) || !(period > 0.0) || !std::isfinite(count) || - count < 1.0 || - count > static_cast(step_.maximum_increments) || - h * count != period) { - InputFailure("invalid-static-schedule", block.data[0].location, - block.canonical_name, "DIRECT", - "STATIC DIRECT requires positive exact N*h=T and N<=INC."); - return; - } - step_.static_values = {h, period, h, h}; - step_.static_direct = true; - step_.static_increment_count = static_cast(count); - step_.has_static = true; - return; - } - if (step_.perturbation_explicit || step_.frequency) { - InputFailure("unsupported-analysis-procedure", block.location, - block.canonical_name, "PERTURBATION", - "PERTURBATION is supported only with FREQUENCY."); - return; - } - if (step_.has_heat_transfer || step_.has_static || - !ValidateParameters(block, {}) || block.data.size() != 1U || - block.data[0].fields.size() != 4U) { - if (!failure_) { - InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data" - : "invalid-static-data", - block.location, block.canonical_name, "", - "STATIC requires exactly one row of four values."); - } - return; - } - for (std::size_t field = 0U; field < 4U; ++field) { - const std::string& text = block.data[0].fields[field]; - const bool parsed = - step_.nlgeom - ? ParseNonlinearStaticDouble(text, step_.static_values[field], - block.data[0].location, - block.canonical_name) - : ParseDouble(text, step_.static_values[field], - block.data[0].location, block.canonical_name); - if (!parsed) { - return; - } - if (step_.static_values[field] <= 0.0) { - InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data" - : "invalid-static-data", - block.data[0].location, block.canonical_name, "", - "All four STATIC fields must be positive."); - return; - } - } - if (step_.static_values[2] > step_.static_values[3]) { - InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data" - : "invalid-static-data", - block.data[0].location, block.canonical_name, "", - "STATIC minimum increment cannot exceed maximum increment."); - return; - } - if (step_.nlgeom && step_.static_values[1] != 1.0) { - InputFailure("invalid-nonlinear-static-data", block.data[0].location, - block.canonical_name, "", - "The nonlinear STATIC time period must be exactly 1.0."); - return; - } - step_.has_static = true; - } - - void ParseOutputRoot(const KeywordBlock& block) { - if (step_.has_heat_transfer) { - const auto* field = Parameter(block, "FIELD"); - const auto* history = Parameter(block, "HISTORY"); - const auto* variable = Parameter(block, "VARIABLE"); - const auto* frequency = Parameter(block, "FREQUENCY"); - const bool valid_field = - field && !field->value && variable && variable->value && - AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"); - const bool valid_history = history && !history->value && frequency && - frequency->value && *frequency->value == "0"; - if (!RequireNoData(block)) return; - if (block.parameters.size() != 2U || (!valid_field && !valid_history)) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "", - "Thermal OUTPUT accepts FIELD, VARIABLE=PRESELECT or " - "HISTORY, FREQUENCY=0."); - return; - } - AddIgnoredWarning(block); - return; - } - if (!ValidateParameters( - block, (step_.dynamic || step_.has_heat_transfer) - ? std::vector{"FIELD", "HISTORY", - "VARIABLE", "FREQUENCY"} - : std::vector{"FIELD", "HISTORY", - "VARIABLE"})) { - return; - } - const auto* field = Parameter(block, "FIELD"); - const auto* history = Parameter(block, "HISTORY"); - const auto* variable = Parameter(block, "VARIABLE"); - if ((field == nullptr) == (history == nullptr) || - (field != nullptr && field->value) || - (history != nullptr && history->value)) { - InputFailure( - step_.nlgeom ? "invalid-keyword-parameter" : "unsupported-keyword", - block.location, block.canonical_name, "", - "OUTPUT must select exactly FIELD or HISTORY."); - return; - } - if (model_element_family_ == ElementFamily::kSolid && - ((variable && - (!variable->value || - !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) || - !RequireNoData(block))) { - if (!failure_) - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "VARIABLE", - "Solid OUTPUT accepts only PRESELECT requests."); - return; - } - if (step_.nlgeom && - (variable == nullptr || !variable->value || - !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) { - InputFailure("invalid-keyword-parameter", block.location, - block.canonical_name, "VARIABLE", - "Nonlinear OUTPUT requires VARIABLE=PRESELECT."); - return; - } - if (step_.dynamic) { - const auto* frequency = Parameter(block, "FREQUENCY"); - if (!ValidateParameters(block, - {"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"}) || - variable == nullptr || !variable->value || - !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT") || - (frequency != nullptr && - (!frequency->value || *frequency->value != "1")) || - !RequireNoData(block)) { - if (!failure_) { - InputFailure( - "unsupported-keyword", block.location, block.canonical_name, "", - "Dynamic OUTPUT accepts only VARIABLE=PRESELECT, FREQUENCY=1."); - } - return; - } - } else if (step_.frequency && - ((variable != nullptr && - (!variable->value || !AsciiCaseInsensitiveEquals( - *variable->value, "PRESELECT"))) || - !RequireNoData(block))) { - if (!failure_) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "", - "Frequency OUTPUT accepts only VARIABLE=PRESELECT."); - } - return; - } - active_output_ = true; - active_output_is_field_ = field != nullptr; - AddIgnoredWarning(block); - } - - void ParseOutputChild(const KeywordBlock& block) { - if (step_.has_heat_transfer) { - InputFailure("unsupported-keyword", block.location, block.canonical_name, - "", - "Thermal output children are outside the approved subset."); - return; - } - if (!active_output_) { - InputFailure( - "invalid-keyword-location", block.location, block.canonical_name, "", - "Output variable keywords require an active OUTPUT request."); - return; - } - if (model_element_family_ == ElementFamily::kSolid && step_.dynamic) { - InputFailure( - "unsupported-keyword", block.location, block.canonical_name, "", - "Solid dynamic output children are outside the approved subset."); - return; - } - if (model_element_family_ == ElementFamily::kSolid && step_.has_static) { - const auto* variable = Parameter(block, "VARIABLE"); - const auto* directions = Parameter(block, "DIRECTIONS"); - const bool node = block.canonical_name == "NODE OUTPUT"; - const bool element = block.canonical_name == "ELEMENT OUTPUT"; - const bool contact = block.canonical_name == "CONTACT OUTPUT"; - if (!active_output_is_field_ || (!node && !element && !contact) || - !ValidateParameters( - block, - element ? std::vector{"VARIABLE", "DIRECTIONS"} - : std::vector{"VARIABLE"}) || - (directions && - (!directions->value || - !AsciiCaseInsensitiveEquals(*directions->value, "YES"))) || - (variable && - (!variable->value || - !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) || - (contact && (!variable || !block.data.empty()))) { - if (!failure_) - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "", - "Solid static output child is unsupported."); - return; - } - for (const auto& row : block.data) { - for (const auto& field : row.fields) { - const std::string token = CanonicalizeAscii(field); - if (!(node ? (token == "U" || token == "RF") - : (token == "S" || token == "E"))) { - InputFailure("unsupported-keyword", row.location, - block.canonical_name, field, - "Solid output quantities must be physical supported " - "components."); - return; - } - } - } - } - if (step_.frequency) { - const std::string_view required_variable = - block.canonical_name == "NODE OUTPUT" ? "PRESELECT" : "ALL"; - const auto* variable = Parameter(block, "VARIABLE"); - if ((block.canonical_name != "NODE OUTPUT" && - block.canonical_name != "ENERGY OUTPUT") || - (block.canonical_name == "NODE OUTPUT" && !active_output_is_field_) || - (block.canonical_name == "ENERGY OUTPUT" && - active_output_is_field_) || - !ValidateParameters(block, {"VARIABLE"}) || variable == nullptr || - !variable->value || - !AsciiCaseInsensitiveEquals(*variable->value, required_variable) || - !RequireNoData(block)) { - if (!failure_) { - InputFailure( - "unsupported-keyword", block.location, block.canonical_name, "", - "Frequency output child is outside the approved subset."); - } - return; - } - } - AddIgnoredWarning(block); - } - - void AddIgnoredWarning(const KeywordBlock& block) { - // One warning per allowlisted keyword keeps no-op provenance stable; - // subordinate variable rows remain attached to that keyword record. - definition_.warnings.push_back( - {Severity::kWarning, - step_.dynamic ? "ignored-output-request" : "ignored-input-keyword", - block.location, block.canonical_name, "", - "The allowlisted Abaqus keyword is " - "ignored without semantic effect."}); - } - - void RejectUnknown(const KeywordBlock& block) { - if (block.canonical_name == "DLOAD" && - model_element_family_ == ElementFamily::kShell) { - InputFailure("unsupported-distributed-load", block.location, - block.canonical_name, "", - "Distributed, pressure, gravity, body, edge, and follower " - "loads are unsupported."); - return; - } - InputFailure("unsupported-keyword", block.location, block.canonical_name, - "", "The keyword is outside the approved Abaqus subset."); - } - - const RawPart* FindPart(const std::string& name) const { - const auto found = std::find_if( - parts_.begin(), parts_.end(), [&name](const RawPart& part) { - return AsciiCaseInsensitiveEquals(part.name, name); - }); - return found == parts_.end() ? nullptr : &*found; - } - - const RawInstance* FindInstance(const std::string& name) const { - const auto found = - std::find_if(instances_.begin(), instances_.end(), - [&name](const RawInstance& instance) { - return AsciiCaseInsensitiveEquals(instance.name, name); - }); - return found == instances_.end() ? nullptr : &*found; - } - - std::optional FindMaterialIndex(const std::string& name) const { - for (std::size_t index = 0U; index < materials_.size(); ++index) { - if (AsciiCaseInsensitiveEquals(materials_[index].name, name)) { - return static_cast(index); - } - } - return std::nullopt; - } - - const RawSet* FindSet(const std::vector& sets, - const std::string& name) const { - const auto found = - std::find_if(sets.begin(), sets.end(), [&name](const RawSet& set) { - return AsciiCaseInsensitiveEquals(set.name, name); - }); - return found == sets.end() ? nullptr : &*found; - } - - const RawNode* FindNode(const RawPart& part, std::int64_t label) const { - const auto found = std::find_if( - part.nodes.begin(), part.nodes.end(), - [label](const RawNode& node) { return node.label == label; }); - return found == part.nodes.end() ? nullptr : &*found; - } - - const RawElement* FindElement(const RawPart& part, std::int64_t label) const { - const auto found = std::find_if( - part.elements.begin(), part.elements.end(), - [label](const RawElement& element) { return element.label == label; }); - return found == part.elements.end() ? nullptr : &*found; + topology_.AssemblySets().push_back(std::move(set)); } void ValidateCardinality() { - if (parts_.empty() || !assembly_seen_ || instances_.empty() || - materials_.empty() || !step_seen_ || - (!step_.has_static && !step_.frequency && !step_.dynamic && - !step_.has_heat_transfer)) { + if (topology_.Parts().empty() || !assembly_seen_ || + topology_.Instances().empty() || material_properties_.Empty() || + !steps_.HasStep() || !steps_.HasSupportedProcedure()) { const auto location = input_.blocks.empty() ? SourceLocation{input_.source_path, 0U} : input_.blocks.back().location; @@ -3266,1321 +1215,19 @@ class DomainMappingContext::Impl { } } - void FinalizeMaterials() { - for (const auto& material : materials_) { - if (model_element_family_ == ElementFamily::kThermalShell) { - if (!material.density || !material.conductivity || - !material.specific_heat) { - ModelFailure("missing-thermal-material-property", material.location, - "MATERIAL", material.name, - "DS4 materials require DENSITY, CONDUCTIVITY, and " - "SPECIFIC HEAT."); - return; - } - definition_.thermal_materials.push_back({{"", 0, material.name}, - material.name, - *material.conductivity, - *material.density, - *material.specific_heat, - material.location}); - continue; - } - if (!material.has_elastic) { - InputFailure("unresolved-reference", material.location, "MATERIAL", - material.name, - "A material must own exactly one ELASTIC row."); - return; - } - if (model_element_family_ == ElementFamily::kShell || - model_element_family_ == ElementFamily::kSolid) { - if (!(material.youngs_modulus > 0.0) || - !(material.poisson_ratio > -1.0) || - !(material.poisson_ratio < 0.5)) { - ModelFailure(model_element_family_ == ElementFamily::kSolid - ? "invalid-solid-material" - : "invalid-shell-material", - material.elastic_location, "ELASTIC", material.name, - "Isotropic elasticity requires E>0 and -1 0.0) || !std::isfinite(shear_modulus) || - !(shear_modulus > 0.0)) { - ModelFailure("invalid-beam-property", material.elastic_location, - "ELASTIC", material.name, - "E and the derived G=E/(2*(1+nu)) must be positive."); - return; - } - definition_.materials.push_back({material.name, material.youngs_modulus, - material.poisson_ratio, - material.location}); - } - } - - void FinalizeDensities() { - if (model_element_family_ == ElementFamily::kThermalShell) { - for (const auto& block : input_.blocks) { - if (block.canonical_name != "PREPRINT") continue; - if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"})) - return; - if (block.parameters.size() != 4U) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "", - "Thermal PREPRINT requires four NO parameters."); - return; - } - for (const auto& parameter : block.parameters) { - if (!parameter.value || - !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, parameter.name, - "Thermal PREPRINT parameters must equal NO."); - return; - } - } - } - return; - } - if (physical_constants_location_) { - InputFailure("unsupported-keyword", *physical_constants_location_, - "PHYSICAL CONSTANTS", "", - "PHYSICAL CONSTANTS is supported only by thermal input."); - return; - } - if (!step_.frequency && !step_.dynamic && - model_element_family_ == ElementFamily::kSolid) { - for (std::size_t index = 0U; index < materials_.size(); ++index) { - if (materials_[index].density) - definition_.densities.push_back({static_cast(index), - *materials_[index].density, - materials_[index].density_location}); - } - return; - } - if (!step_.frequency && !step_.dynamic) { - if (std::any_of(materials_.begin(), materials_.end(), - [](const RawMaterial& material) { - return material.density.has_value(); - })) { - InputFailure("invalid-density-data", step_.location, "DENSITY", "", - "DENSITY is supported only by the FREQUENCY procedure."); - } - return; - } - if (step_.frequency) { - ValidateFrequencyPreprint(); - if (failure_) { - return; - } - } - if (step_.dynamic) { - ValidateDynamicPreprint(); - if (failure_) { - return; - } - for (auto& warning : definition_.warnings) { - if (warning.keyword == "PREPRINT") { - warning.code = "ignored-output-request"; - } - } - } - if (model_element_family_ != ElementFamily::kShell && - model_element_family_ != ElementFamily::kSolid) { - InputFailure("unsupported-analysis-procedure", step_.location, - step_.frequency ? "FREQUENCY" : "DYNAMIC", step_.name_raw, - "The procedure requires an approved Shell or Solid model."); - return; - } - if (!element_surfaces_.empty()) { - InputFailure("unsupported-keyword", element_surfaces_.front().location, - "SURFACE", element_surfaces_.front().name, - "The selected procedure does not support element surfaces."); - return; - } - for (std::size_t index = 0U; index < materials_.size(); ++index) { - const RawMaterial& material = materials_[index]; - if (!material.density) { - bool active = false; - for (const auto& section : definition_.shell_sections) { - if (section.material_index == static_cast(index)) { - active = true; - break; - } - } - for (const auto& section : definition_.solid_sections) { - if (section.MaterialIndex() == static_cast(index)) - active = true; - } - if (active) { - ModelFailure( - "missing-material-density", material.location, "MATERIAL", - material.name, - "Each active dynamic or frequency material requires DENSITY."); - return; - } - continue; - } - definition_.densities.push_back({static_cast(index), - *material.density, - material.density_location}); - } - } - - void ValidateFrequencyPreprint() { - for (const auto& block : input_.blocks) { - if (block.canonical_name != "PREPRINT") { - continue; - } - if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) || - block.parameters.size() != 4U) { - if (!failure_) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "", - "FREQUENCY requires PREPRINT with ECHO, MODEL, HISTORY, " - "and CONTACT parameters."); - } - return; - } - for (const auto& parameter : block.parameters) { - if (!parameter.value || - !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, parameter.name, - "FREQUENCY requires PREPRINT parameters to equal NO."); - return; - } - } - } - for (auto& warning : definition_.warnings) { - if (warning.keyword == "PREPRINT") { - warning.message = "mandatory-eigen-output-unchanged"; - } - } - } - - void ValidateDynamicPreprint() { - for (const auto& block : input_.blocks) { - if (block.canonical_name != "PREPRINT") { - continue; - } - if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) || - block.parameters.size() != 4U) { - if (!failure_) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "", - "DYNAMIC requires PREPRINT with ECHO, MODEL, HISTORY, " - "and CONTACT parameters."); - } - return; - } - for (const auto& parameter : block.parameters) { - if (!parameter.value || - !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, parameter.name, - "DYNAMIC requires PREPRINT parameters to equal NO."); - return; - } - } - } - } - - void FinalizePartsAndSections() { - part_section_assignments_.resize(parts_.size()); - part_shell_section_assignments_.resize(parts_.size()); - const bool shell_model = - model_element_family_ == ElementFamily::kShell || - model_element_family_ == ElementFamily::kThermalShell; - for (std::size_t part_index = 0U; part_index < parts_.size(); - ++part_index) { - const RawPart& part = parts_[part_index]; - if (part.nodes.empty() || part.elements.empty() || - (!shell_model && model_element_family_ != ElementFamily::kSolid && - part.sections.empty())) { - InputFailure("invalid-model-cardinality", part.location, "PART", - part.name, - "A part requires nodes, elements, and its approved " - "section form."); - return; - } - - PartDefinition part_definition{}; - part_definition.name = part.name; - part_definition.location = part.location; - for (const auto& node : part.nodes) { - part_definition.node_source_labels.push_back(node.label); - } - for (const auto& element : part.elements) { - part_definition.element_source_labels.push_back(element.label); - for (const auto node_label : element.node_labels) { - if (FindNode(part, node_label) == nullptr) { - InputFailure(shell_model ? "invalid-shell-connectivity" - : "unresolved-reference", - element.location, "ELEMENT", element.label_text, - "Element connectivity must resolve within its part."); - return; - } - } - } - for (const auto& set : part.node_sets) { - part_definition.node_set_names.push_back(set.name); - for (const auto label : set.members) { - if (FindNode(part, label) == nullptr) { - InputFailure("unresolved-reference", set.location, "NSET", set.name, - "Every node-set member must resolve within its part."); - return; - } - } - } - for (const auto& set : part.element_sets) { - part_definition.element_set_names.push_back(set.name); - for (const auto label : set.members) { - if (FindElement(part, label) == nullptr) { - InputFailure( - "unresolved-reference", set.location, "ELSET", set.name, - "Every element-set member must resolve within its part."); - return; - } - } - } - definition_.parts.push_back(std::move(part_definition)); - - if (model_element_family_ == ElementFamily::kSolid) { - auto& assignments = part_shell_section_assignments_[part_index]; - for (const auto& section : part.solid_sections) { - const auto* element_set = - FindSet(part.element_sets, section.element_set_name); - const auto material = FindMaterialIndex(section.material_name); - if (!element_set || !material) { - InputFailure("unresolved-solid-section", section.location, - "SOLID SECTION", section.element_set_name, - "Solid ELSET and MATERIAL must resolve."); - return; - } - const auto index = - static_cast(definition_.solid_sections.size()); - definition_.solid_sections.emplace_back( - SourceEntityId{part.name, static_cast(index), - section.element_set_name}, - section.element_set_name, *material, section.location); - for (const auto label : element_set->members) { - if (!assignments.emplace(label, std::make_pair(index, *material)) - .second) { - InputFailure( - "invalid-solid-section-assignment", section.location, - "SOLID SECTION", std::to_string(label), - "Solid elements require exactly one section assignment."); - return; - } - } - } - for (const auto& element : part.elements) { - if (assignments.find(element.label) == assignments.end()) { - InputFailure("invalid-solid-section-assignment", element.location, - "ELEMENT", element.label_text, - "A Solid section assignment is missing."); - return; - } - } - continue; - } - if (shell_model) { - auto& assignments = part_shell_section_assignments_[part_index]; - for (const auto& section : part.shell_sections) { - const auto* element_set = - FindSet(part.element_sets, section.element_set_name); - const auto material_index = FindMaterialIndex(section.material_name); - if (element_set == nullptr || !material_index) { - InputFailure("unresolved-shell-section", section.location, - "SHELL SECTION", section.element_set_name, - "Shell section ELSET and MATERIAL references " - "must resolve."); - return; - } - const EntityIndex section_index = - model_element_family_ == ElementFamily::kThermalShell - ? static_cast( - definition_.thermal_sections.size()) - : static_cast(definition_.shell_sections.size()); - if (model_element_family_ == ElementFamily::kThermalShell) { - definition_.thermal_sections.push_back( - {{part.name, static_cast(section_index), - section.element_set_name}, - section.element_set_name, - section.thickness, - *material_index, - section.location}); - } else { - definition_.shell_sections.push_back( - {section.element_set_name, section.thickness, *material_index, - section.location}); - } - for (const auto element_label : element_set->members) { - if (!assignments - .emplace(element_label, - std::make_pair(section_index, *material_index)) - .second) { - InputFailure("invalid-shell-section-assignment", section.location, - "SHELL SECTION", std::to_string(element_label), - "A shell element cannot receive multiple " - "section assignments."); - return; - } - } - } - for (const auto& element : part.elements) { - if (assignments.find(element.label) == assignments.end()) { - InputFailure("invalid-shell-section-assignment", element.location, - "ELEMENT", element.label_text, - "Every shell element requires exactly one " - "resolved section assignment."); - return; - } - } - continue; - } - - auto& assignments = part_section_assignments_[part_index]; - for (const auto& section : part.sections) { - const auto* element_set = - FindSet(part.element_sets, section.element_set_name); - const auto material_index = FindMaterialIndex(section.material_name); - if (element_set == nullptr || !material_index) { - InputFailure("unresolved-reference", section.location, - "BEAM GENERAL SECTION", section.element_set_name, - "Section ELSET and MATERIAL references must resolve."); - return; - } - if (section.properties[2] != 0.0) { - ModelFailure("unsupported-coupled-section", section.location, - "BEAM GENERAL SECTION", section.element_set_name, - "V0 requires exact I12=0."); - return; - } - if (!(section.properties[0] > 0.0) || !(section.properties[1] > 0.0) || - !(section.properties[3] > 0.0) || !(section.properties[4] > 0.0)) { - ModelFailure("invalid-beam-property", section.location, - "BEAM GENERAL SECTION", section.element_set_name, - "A, I11, I22, and J must be positive."); - return; - } - const EntityIndex section_index = - static_cast(definition_.sections.size()); - definition_.sections.push_back( - {section.element_set_name, section.properties[0], - section.properties[1], section.properties[2], - section.properties[3], section.properties[4], section.first_axis, - section.section_points, section.location}); - for (const auto element_label : element_set->members) { - if (!assignments - .emplace(element_label, - std::make_pair(section_index, *material_index)) - .second) { - InputFailure("duplicate-entity", section.location, - "BEAM GENERAL SECTION", std::to_string(element_label), - "An element cannot receive multiple section " - "assignments."); - return; - } - } - } - for (const auto& element : part.elements) { - if (assignments.find(element.label) == assignments.end()) { - InputFailure( - "unresolved-reference", element.location, "ELEMENT", - element.label_text, - "Every B33 element requires a resolved section assignment."); - return; - } - } - } - } - - std::size_t PartIndex(const RawPart& part) const { - return static_cast(&part - parts_.data()); - } - - bool ValidateGeometry(const RawElement& raw, const Node& first, - const Node& second, const GeneralBeamSection& section) { - const auto maximum_absolute = [](const Vector3& vector) { - return std::max( - {std::abs(vector[0]), std::abs(vector[1]), std::abs(vector[2])}); - }; - const Vector3 first_position{first.coordinates}; - const Vector3 second_position{second.coordinates}; - - // Compare both approved inequalities after a common scaling. This - // preserves the exact ratios while avoiding overflow in x*x and in - // subtraction between large finite coordinates. - const double global_coordinate_scale = - std::max({1.0, maximum_absolute(first_position), - maximum_absolute(second_position)}); - const Vector3 first_scaled = first_position / global_coordinate_scale; - const Vector3 second_scaled = second_position / global_coordinate_scale; - const Vector3 delta_scaled = second_scaled - first_scaled; - const double length_ratio = delta_scaled.Norm(); - const double coordinate_norm_ratio = - std::max({1.0 / global_coordinate_scale, first_scaled.Norm(), - second_scaled.Norm()}); - if (!(length_ratio > 1.0e-12 * coordinate_norm_ratio)) { - return ModelFailure( - "invalid-beam-length", raw.location, "ELEMENT", raw.label_text, - "Beam length fails the approved scale-aware threshold."); - } - const Vector3 tangent = delta_scaled / length_ratio; - - const Vector3 guide{section.first_axis}; - const double global_guide_scale = std::max(1.0, maximum_absolute(guide)); - const Vector3 guide_scaled = guide / global_guide_scale; - const double projection = guide_scaled.Dot(tangent); - const Vector3 perpendicular = guide_scaled - projection * tangent; - const double guide_norm_ratio = - std::max(1.0 / global_guide_scale, guide_scaled.Norm()); - if (!(perpendicular.Norm() > 1.0e-12 * guide_norm_ratio)) { - return ModelFailure( - "invalid-beam-guide-vector", section.location, "BEAM GENERAL SECTION", - raw.label_text, - "The first section axis cannot be zero or tangent-parallel."); - } - return true; - } - - void ExpandInstances() { - for (const auto& raw_instance : instances_) { - const RawPart* part = FindPart(raw_instance.part_name); - if (part == nullptr) { - InputFailure("unresolved-reference", raw_instance.location, "INSTANCE", - raw_instance.name, - "INSTANCE PART must resolve case-insensitively."); - return; - } - InstanceDefinition instance{ - raw_instance.name, part->name, {}, {}, raw_instance.location}; - std::map node_indices; - std::map element_indices; - - // Instance order, followed by part-local declaration order, is the - // sole source of stable expanded internal IDs. - for (const auto& raw_node : part->nodes) { - if (definition_.nodes.size() > - std::numeric_limits::max()) { - InputFailure("entity-index-overflow", raw_node.location, "NODE", - raw_node.label_text, - "Expanded node count exceeds EntityIndex capacity."); - return; - } - const EntityIndex index = - static_cast(definition_.nodes.size()); - definition_.nodes.push_back( - {{raw_instance.name, raw_node.label, raw_node.label_text}, - raw_node.coordinates, - raw_node.location}); - node_indices.emplace(raw_node.label, index); - instance.node_mappings.push_back({raw_node.label, index}); - } - - for (const auto& raw_element : part->elements) { - EntityIndex index = 0U; - if (raw_element.type == RawElement::Type::kB33) { - const auto first = node_indices.find(raw_element.node_labels[0]); - const auto second = node_indices.find(raw_element.node_labels[1]); - const auto& assignments = part_section_assignments_[PartIndex(*part)]; - const auto assignment = assignments.find(raw_element.label); - if (first == node_indices.end() || second == node_indices.end() || - assignment == assignments.end()) { - InputFailure( - "unresolved-reference", raw_element.location, "ELEMENT", - raw_element.label_text, - "Expanded connectivity and section assignment must resolve."); - return; - } - if (definition_.elements.size() > - std::numeric_limits::max()) { - InputFailure( - "entity-index-overflow", raw_element.location, "ELEMENT", - raw_element.label_text, - "Expanded element count exceeds EntityIndex capacity."); - return; - } - index = static_cast(definition_.elements.size()); - const auto& section = definition_.sections[assignment->second.first]; - if (!ValidateGeometry(raw_element, definition_.nodes[first->second], - definition_.nodes[second->second], section)) { - return; - } - definition_.elements.push_back( - {{raw_instance.name, raw_element.label, raw_element.label_text}, - {first->second, second->second}, - assignment->second.second, - assignment->second.first, - raw_element.location}); - } else if (raw_element.type == RawElement::Type::kC3d8) { - std::array connected_nodes{}; - for (std::size_t node = 0U; node < connected_nodes.size(); ++node) { - const auto found = node_indices.find(raw_element.node_labels[node]); - if (found == node_indices.end()) { - InputFailure("invalid-solid-connectivity", raw_element.location, - "ELEMENT", raw_element.label_text, - "Solid nodes must resolve."); - return; - } - connected_nodes[node] = found->second; - } - const auto& assignments = - part_shell_section_assignments_[PartIndex(*part)]; - const auto assignment = assignments.find(raw_element.label); - if (assignment == assignments.end() || - definition_.solid_elements.size() > - std::numeric_limits::max()) { - InputFailure("invalid-solid-section-assignment", - raw_element.location, "ELEMENT", - raw_element.label_text, - "Solid assignment must resolve."); - return; - } - index = static_cast(definition_.solid_elements.size()); - definition_.solid_elements.emplace_back( - SourceEntityId{raw_instance.name, raw_element.label, - raw_element.label_text}, - connected_nodes, assignment->second.second, - assignment->second.first, raw_element.location); - } else { - std::array connected_nodes{}; - for (std::size_t node = 0U; node < connected_nodes.size(); ++node) { - const auto found = node_indices.find(raw_element.node_labels[node]); - if (found == node_indices.end()) { - InputFailure("invalid-shell-connectivity", raw_element.location, - "ELEMENT", raw_element.label_text, - "Expanded shell connectivity must resolve."); - return; - } - connected_nodes[node] = found->second; - } - const auto& assignments = - part_shell_section_assignments_[PartIndex(*part)]; - const auto assignment = assignments.find(raw_element.label); - if (assignment == assignments.end()) { - InputFailure( - "invalid-shell-section-assignment", raw_element.location, - "ELEMENT", raw_element.label_text, - "Expanded shell section assignment must resolve exactly once."); - return; - } - if (raw_element.type == RawElement::Type::kDs4) { - if (definition_.thermal_elements.size() > - std::numeric_limits::max()) { - InputFailure("entity-index-overflow", raw_element.location, - "ELEMENT", raw_element.label_text, - "Expanded thermal element count exceeds EntityIndex " - "capacity."); - return; - } - index = - static_cast(definition_.thermal_elements.size()); - definition_.thermal_elements.push_back( - {{raw_instance.name, raw_element.label, raw_element.label_text}, - connected_nodes, - assignment->second.first, - raw_element.location}); - element_indices.emplace(raw_element.label, index); - instance.element_mappings.push_back({raw_element.label, index}); - continue; - } - if (definition_.shell_elements.size() > - std::numeric_limits::max()) { - InputFailure( - "entity-index-overflow", raw_element.location, "ELEMENT", - raw_element.label_text, - "Expanded shell element count exceeds EntityIndex capacity."); - return; - } - index = static_cast(definition_.shell_elements.size()); - definition_.shell_elements.push_back( - {{raw_instance.name, raw_element.label, raw_element.label_text}, - raw_element.type == RawElement::Type::kS4 - ? ShellSourceElementType::kS4 - : ShellSourceElementType::kS4r, - connected_nodes, - assignment->second.second, - assignment->second.first, - raw_element.location}); - } - element_indices.emplace(raw_element.label, index); - instance.element_mappings.push_back({raw_element.label, index}); - } - - for (const auto& raw_set : part->node_sets) { - NodeSet set{raw_set.name, raw_instance.name, {}, raw_set.location}; - for (const auto member : raw_set.members) { - const auto found = node_indices.find(member); - if (found == node_indices.end()) { - InputFailure("unresolved-reference", raw_set.location, "NSET", - raw_set.name, "Part node-set expansion failed."); - return; - } - set.node_indices.push_back(found->second); - } - definition_.node_sets.push_back(std::move(set)); - } - for (const auto& raw_set : part->element_sets) { - ElementSet set{raw_set.name, raw_instance.name, {}, raw_set.location}; - for (const auto member : raw_set.members) { - const auto found = element_indices.find(member); - if (found == element_indices.end()) { - InputFailure("unresolved-reference", raw_set.location, "ELSET", - raw_set.name, "Part element-set expansion failed."); - return; - } - set.element_indices.push_back(found->second); - } - definition_.element_sets.push_back(std::move(set)); - } - definition_.instances.push_back(std::move(instance)); - } - } - - void ExpandAssemblySets() { - for (const auto& raw_set : assembly_sets_) { - const RawInstance* raw_instance = FindInstance(raw_set.instance_name); - if (raw_instance == nullptr) { - InputFailure("unresolved-reference", raw_set.location, - raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name, - "Assembly set INSTANCE must resolve."); - return; - } - const auto definition_instance = std::find_if( - definition_.instances.begin(), definition_.instances.end(), - [&raw_instance](const InstanceDefinition& instance) { - return AsciiCaseInsensitiveEquals(instance.name, - raw_instance->name); - }); - if (definition_instance == definition_.instances.end()) { - InputFailure("unresolved-reference", raw_set.location, - raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name, - "Assembly set instance expansion is unavailable."); - return; - } - if (raw_set.is_node_set) { - definition_.node_sets.erase( - std::remove_if( - definition_.node_sets.begin(), definition_.node_sets.end(), - [&raw_set](const NodeSet& set) { - return AsciiCaseInsensitiveEquals(set.name, raw_set.name); - }), - definition_.node_sets.end()); - NodeSet set{ - raw_set.name, definition_instance->name, {}, raw_set.location}; - for (const auto member : raw_set.members) { - const auto mapping = - std::find_if(definition_instance->node_mappings.begin(), - definition_instance->node_mappings.end(), - [member](const SourceIndexMapping& value) { - return value.source_label == member; - }); - if (mapping == definition_instance->node_mappings.end()) { - InputFailure("unresolved-reference", raw_set.location, "NSET", - raw_set.name, - "Assembly node-set member must resolve in its " - "instance."); - return; - } - set.node_indices.push_back(mapping->internal_index); - } - definition_.node_sets.push_back(std::move(set)); - } else { - definition_.element_sets.erase( - std::remove_if(definition_.element_sets.begin(), - definition_.element_sets.end(), - [&raw_set](const ElementSet& set) { - return AsciiCaseInsensitiveEquals(set.name, - raw_set.name); - }), - definition_.element_sets.end()); - ElementSet set{ - raw_set.name, definition_instance->name, {}, raw_set.location}; - for (const auto member : raw_set.members) { - const auto mapping = - std::find_if(definition_instance->element_mappings.begin(), - definition_instance->element_mappings.end(), - [member](const SourceIndexMapping& value) { - return value.source_label == member; - }); - if (mapping == definition_instance->element_mappings.end()) { - InputFailure("unresolved-reference", raw_set.location, "ELSET", - raw_set.name, - "Assembly element-set member must resolve in " - "its instance."); - return; - } - set.element_indices.push_back(mapping->internal_index); - } - definition_.element_sets.push_back(std::move(set)); - } - } - } - - bool AppendSurfaceFacet( - const RawSurfaceMember& member, const RawInstance& raw_instance, - std::size_t instance_ordinal, EntityIndex element_index, - SurfaceMemberKind member_kind, const std::string& elset_name_raw, - std::vector>* - candidates) { - const bool thermal = model_element_family_ == ElementFamily::kThermalShell; - const auto element_count = thermal ? definition_.thermal_elements.size() - : definition_.shell_elements.size(); - if (static_cast(element_index) >= element_count) { - return InputFailure( - "unsupported-element-formulation", member.location, "SURFACE", - member.target, - "Element surfaces may resolve only to S4 or S4R shell elements."); - } - const RawPart* part = FindPart(raw_instance.part_name); - if (part == nullptr) { - return InputFailure( - "unresolved-reference", member.location, "SURFACE", member.target, - "The surface instance must resolve to its source part."); - } - const auto& source_id = - thermal ? definition_.thermal_elements[element_index].source_id - : definition_.shell_elements[element_index].source_id; - const std::string source_type = - thermal ? "DS4" - : (definition_.shell_elements[element_index].source_type == - ShellSourceElementType::kS4 - ? "S4" - : "S4R"); - ElementSurfaceFacetDefinition facet{element_index, - 0U, - part->name, - CanonicalizeAscii(part->name), - raw_instance.name, - CanonicalizeAscii(raw_instance.name), - source_id.source_label, - source_id.source_label_text, - source_type, - member.side, - member.location, - member.ordinal, - member_kind, - elset_name_raw, - member_kind == SurfaceMemberKind::kElset - ? CanonicalizeAscii(elset_name_raw) - : ""}; - candidates->push_back({std::move(facet), instance_ordinal}); - return true; - } - - bool ResolveDirectSurfaceMember( - const RawSurfaceMember& member, - std::vector>* - candidates) { - const std::size_t dot = member.target.find('.'); - if (dot == std::string::npos || dot == 0U || - dot + 1U >= member.target.size() || - member.target.find('.', dot + 1U) != std::string::npos) { - return InputFailure( - "unresolved-reference", member.location, "SURFACE", member.target, - "A direct surface member must be instance.positive-element-label."); - } - const std::string instance_name = member.target.substr(0U, dot); - if (!IsPressureNameToken(instance_name)) { - return InputFailure( - "unsupported-surface-target", member.location, "SURFACE", - member.target, - "A direct surface member must use an identity instance name."); - } - std::int64_t source_label = 0; - if (!ParseInteger(member.target.substr(dot + 1U), source_label, - member.location, "SURFACE", true)) { - return false; - } - const auto raw_instance = std::find_if( - instances_.begin(), instances_.end(), - [&instance_name](const RawInstance& value) { - return AsciiCaseInsensitiveEquals(value.name, instance_name); - }); - if (raw_instance == instances_.end()) { - if (FindPart(instance_name) != nullptr) { - return InputFailure( - "unsupported-surface-target", member.location, "SURFACE", - member.target, - "A direct surface member cannot use a part name as an instance."); - } - return InputFailure("unresolved-reference", member.location, "SURFACE", - member.target, - "A direct surface member instance must resolve."); - } - const auto instance = std::find_if( - definition_.instances.begin(), definition_.instances.end(), - [&raw_instance](const InstanceDefinition& value) { - return AsciiCaseInsensitiveEquals(value.name, raw_instance->name); - }); - const auto element = std::find_if( - instance->element_mappings.begin(), instance->element_mappings.end(), - [source_label](const SourceIndexMapping& value) { - return value.source_label == source_label; - }); - if (element == instance->element_mappings.end()) { - return InputFailure( - "unresolved-reference", member.location, "SURFACE", member.target, - "A direct surface member must resolve to a supported shell element."); - } - return AppendSurfaceFacet(member, *raw_instance, - static_cast(std::distance( - instances_.begin(), raw_instance)), - element->internal_index, - SurfaceMemberKind::kDirect, "", candidates); - } - - bool ResolveElsetSurfaceMember( - const RawSurfaceMember& member, - std::vector>* - candidates) { - if (!IsPressureNameToken(member.target)) { - return InputFailure("unsupported-surface-target", member.location, - "SURFACE", member.target, - "A surface ELSET must use the approved name syntax."); - } - const auto raw_set = std::find_if( - assembly_sets_.begin(), assembly_sets_.end(), - [&member](const RawAssemblySet& value) { - return !value.is_node_set && - AsciiCaseInsensitiveEquals(value.name, member.target); - }); - if (raw_set == assembly_sets_.end()) { - const bool part_scoped = std::any_of( - parts_.begin(), parts_.end(), [this, &member](const RawPart& part) { - return FindSet(part.element_sets, member.target) != nullptr; - }); - if (part_scoped) { - return InputFailure( - "cross-scope-target", member.location, "SURFACE", member.target, - "A surface ELSET must be declared at assembly scope."); - } - return InputFailure( - "missing-reference", member.location, "SURFACE", member.target, - "A surface ELSET must be assembly-scoped and explicit-instance."); - } - const auto raw_instance = std::find_if( - instances_.begin(), instances_.end(), - [&raw_set](const RawInstance& value) { - return AsciiCaseInsensitiveEquals(value.name, raw_set->instance_name); - }); - if (raw_instance == instances_.end()) { - return InputFailure("unresolved-reference", member.location, "SURFACE", - member.target, - "The surface ELSET instance must resolve."); - } - const auto element_set = std::find_if( - definition_.element_sets.begin(), definition_.element_sets.end(), - [&raw_set](const ElementSet& value) { - return AsciiCaseInsensitiveEquals(value.name, raw_set->name) && - value.instance_name && - AsciiCaseInsensitiveEquals(*value.instance_name, - raw_set->instance_name); - }); - if (element_set == definition_.element_sets.end()) { - return InputFailure( - "unresolved-reference", member.location, "SURFACE", member.target, - "The surface ELSET must expand in its explicit instance."); - } - for (const EntityIndex element_index : element_set->element_indices) { - if (!AppendSurfaceFacet(member, *raw_instance, - static_cast(std::distance( - instances_.begin(), raw_instance)), - element_index, SurfaceMemberKind::kElset, - member.target, candidates)) { - return false; - } - } - return true; - } - - void ResolveElementSurfaces() { - std::vector resolved_surfaces; - resolved_surfaces.reserve(element_surfaces_.size()); - for (const auto& surface : element_surfaces_) { - std::vector> - candidates; - for (const auto& member : surface.members) { - const bool is_direct = member.target.find('.') != std::string::npos; - if (!(is_direct ? ResolveDirectSurfaceMember(member, &candidates) - : ResolveElsetSurfaceMember(member, &candidates))) { - return; - } - } - if (candidates.empty()) { - InputFailure("empty-surface", surface.location, "SURFACE", surface.name, - "An element surface must resolve at least one facet."); - return; - } - - std::vector> - unique_facets; - for (auto& candidate : candidates) { - const auto same_element = - std::find_if(unique_facets.begin(), unique_facets.end(), - [&candidate](const auto& existing) { - return existing.first.instance_name_canonical == - candidate.first.instance_name_canonical && - existing.first.source_element_label == - candidate.first.source_element_label; - }); - if (same_element != unique_facets.end()) { - if (same_element->first.side != candidate.first.side) { - InputFailure( - "conflicting-surface-membership", - candidate.first.first_member_location, "SURFACE", surface.name, - "One shell element cannot appear on both surface sides."); - return; - } - if (model_element_family_ != ElementFamily::kThermalShell) { - definition_.warnings.push_back( - {Severity::kWarning, "duplicate-surface-member-normalized", - candidate.first.first_member_location, "SURFACE", surface.name, - "A repeated same-side surface member was normalized once."}); - } - continue; - } - unique_facets.push_back(std::move(candidate)); - } - std::sort(unique_facets.begin(), unique_facets.end(), - [](const auto& lhs, const auto& rhs) { - const int lhs_side = - lhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1; - const int rhs_side = - rhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1; - return std::tie(lhs.second, lhs.first.source_element_label, - lhs_side) < - std::tie(rhs.second, rhs.first.source_element_label, - rhs_side); - }); - - ElementSurfaceDefinition resolved{surface.name, - CanonicalizeAscii(surface.name), - {}, - surface.location, - surface.declaration_ordinal}; - resolved.facets.reserve(unique_facets.size()); - for (std::size_t ordinal = 0U; ordinal < unique_facets.size(); - ++ordinal) { - unique_facets[ordinal].first.facet_ordinal = ordinal; - resolved.facets.push_back(std::move(unique_facets[ordinal].first)); - } - resolved_surfaces.push_back(std::move(resolved)); - } - definition_.element_surfaces = std::move(resolved_surfaces); - } - - /// @brief Builds one stable node-target index from the completed candidate. - SourceTargetIndex BuildSourceTargetIndex() const { - std::vector entries; - std::size_t declaration_order = 0U; - for (std::size_t node = 0U; node < definition_.nodes.size(); ++node) { - const auto& source_id = definition_.nodes[node].source_id; - entries.push_back({SourceEntityKind::kNode, source_id.instance_name, "", - source_id, static_cast(node), - declaration_order++}); - } - for (const auto& set : definition_.node_sets) { - for (const EntityIndex node : set.node_indices) { - const auto& source_id = definition_.nodes[node].source_id; - entries.push_back({SourceEntityKind::kNode, - set.instance_name.value_or(source_id.instance_name), - set.name, source_id, node, declaration_order++}); - } - } - return SourceTargetIndex{std::move(entries)}; - } - - std::optional> ResolveNodeTarget( - const SourceTargetResolver& resolver, const std::string& target, - const SourceLocation& location, const std::string& keyword) { - auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", target}); - if (!resolved.HasValue()) { - InputFailure( - "unresolved-reference", location, keyword, target, - "The boundary or load target must resolve unambiguously to one " - "node or one node set."); - return std::nullopt; - } - std::vector indices; - indices.reserve(resolved.Value().size()); - for (const auto& entry : resolved.Value()) { - if (model_element_family_ == ElementFamily::kThermalShell && - !thermal_active_nodes_[entry.entity_index]) { - ModelFailure("inactive-thermal-target", location, keyword, target, - "Thermal targets must connect to an active DS4 element."); - return std::nullopt; - } - indices.push_back(entry.entity_index); - } - return indices; - } - - void FinalizeStep() { - if (step_.has_heat_transfer) { - thermal_active_nodes_.assign(definition_.nodes.size(), false); - for (const auto& element : definition_.thermal_elements) - for (const auto node : element.node_indices) - thermal_active_nodes_[node] = true; - const SourceTargetIndex target_index = BuildSourceTargetIndex(); - const SourceTargetResolver resolver{target_index}; - const auto append_boundaries = - [this, &resolver]( - const std::vector& rows, - std::map, double>* values, - std::vector* out) { - for (const auto& row : rows) { - const auto nodes = ResolveNodeTarget(resolver, row.target, - row.location, "BOUNDARY"); - if (!nodes) return false; - for (const EntityIndex node : *nodes) { - const auto key = std::make_pair(node, row.dof); - const auto existing = values->find(key); - if (existing != values->end() && - existing->second != row.value) { - return InputFailure("conflicting-boundary-condition", - row.location, "BOUNDARY", row.target, - "Expanded thermal boundaries prescribe " - "different values."); - } - (*values)[key] = row.value; - out->push_back({node, row.dof, row.value, row.location}); - } - } - return true; - }; - std::map, double> model_values; - if (!append_boundaries(thermal_model_boundaries_, &model_values, - &definition_.thermal_model_boundaries)) - return; - std::vector step_boundaries; - std::map, double> step_values; - if (!append_boundaries(step_.thermal_boundaries, &step_values, - &step_boundaries)) - return; - std::map initial_values; - for (const auto& row : thermal_initial_conditions_) { - const auto nodes = ResolveNodeTarget(resolver, row.target, row.location, - "INITIAL CONDITIONS"); - if (!nodes) return; - for (const EntityIndex node : *nodes) { - const auto inserted = initial_values.emplace(node, row.value); - if (!inserted.second && inserted.first->second != row.value) { - InputFailure( - "conflicting-initial-temperature", row.location, - "INITIAL CONDITIONS", row.target, - "Expanded initial temperatures prescribe different values."); - return; - } - definition_.thermal_initial_conditions.push_back( - {node, row.value, row.location}); - } - } - std::vector loads; - for (const auto& row : step_.thermal_fluxes) { - const auto nodes = - ResolveNodeTarget(resolver, row.target, row.location, "CFLUX"); - if (!nodes) return; - for (const EntityIndex node : *nodes) { - loads.push_back( - {node, row.dof, row.power, row.location, loads.size()}); - } - } - definition_.thermal_step = {step_.name_raw, - step_.thermal_increment, - step_.thermal_period, - step_.maximum_increments, - false, - std::move(step_boundaries), - std::move(loads), - step_.location}; - return; - } - std::vector boundaries = - model_boundaries_; - boundaries.insert(boundaries.end(), step_.boundaries.begin(), - step_.boundaries.end()); - if (step_.frequency) { - ValidateEigenBoundaries(boundaries); - if (failure_) { - return; - } - } - if (step_.dynamic) { - ValidateDynamicBoundaries(boundaries); - if (failure_) { - return; - } - } - const SourceTargetIndex target_index = BuildSourceTargetIndex(); - const SourceTargetResolver target_resolver{target_index}; - - if (model_element_family_ == ElementFamily::kSolid) { - std::vector physical; - for (auto boundary : boundaries) { - if (!ResolveNodeTarget(target_resolver, boundary.target, - boundary.location, "BOUNDARY")) - return; - if (boundary.last_dof > 3) { - if (boundary.value != 0.0) { - InputFailure("unsupported-solid-rotation-boundary", - boundary.location, "BOUNDARY", boundary.target, - "Solid rotational boundaries must be exact zero to be " - "inactive."); - return; - } - definition_.warnings.push_back( - {Severity::kWarning, "inactive-solid-rotation-boundary", - boundary.location, "BOUNDARY", boundary.target, - "Exact-zero rotational boundary DOFs " + - std::to_string(std::max(4, boundary.first_dof)) + ".." + - std::to_string(boundary.last_dof) + - " are inactive; Solid equations contain translations " - "only."}); - boundary.last_dof = 3; - } - if (boundary.first_dof <= boundary.last_dof) - physical.push_back(std::move(boundary)); - } - boundaries = std::move(physical); - std::vector active(definition_.nodes.size(), false); - for (const auto& element : definition_.solid_elements) - for (const auto node : element.node_indices) active[node] = true; - for (std::size_t node = 0U; node < active.size(); ++node) { - if (!active[node]) { - ModelFailure("unused-solid-node", definition_.nodes[node].location, - "NODE", - definition_.nodes[node].source_id.source_label_text, - "Every Solid node must connect to a C3D8 element."); - return; - } - } - } - std::map, double> prescribed_values; - for (const auto& boundary : boundaries) { - auto target = ResolveNodeTarget(target_resolver, boundary.target, - boundary.location, "BOUNDARY"); - if (!target) { - return; - } - for (const auto node : *target) { - for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) { - const auto key = std::make_pair(node, dof); - const auto existing = prescribed_values.find(key); - if (existing != prescribed_values.end() && - existing->second != boundary.value) { - InputFailure("conflicting-boundary-condition", boundary.location, - "BOUNDARY", boundary.target, - "Expanded boundary rows prescribe different values to " - "one node/DOF."); - return; - } - prescribed_values[key] = boundary.value; - } - } - } - for (const auto& load : step_.loads) { - if (!ResolveNodeTarget(target_resolver, load.target, load.location, - "CLOAD")) { - return; - } - } - if (step_.frequency) { - step_.frequency->boundaries = std::move(boundaries); - definition_.frequency_step = std::move(step_.frequency); - return; - } - if (step_.dynamic) { - step_.dynamic->boundaries = std::move(boundaries); - step_.dynamic->loads = step_.loads; - definition_.dynamic_step = std::move(step_.dynamic); - return; - } - std::vector surface_pressure_loads; - surface_pressure_loads.reserve(step_.surface_pressure_loads.size()); - for (const auto& load : step_.surface_pressure_loads) { - const auto surface = std::find_if( - definition_.element_surfaces.begin(), - definition_.element_surfaces.end(), [&load](const auto& candidate) { - return AsciiCaseInsensitiveEquals(candidate.surface_name_raw, - load.surface_name); - }); - if (surface == definition_.element_surfaces.end()) { - InputFailure("missing-reference", load.data_location, "DSLOAD", - load.surface_name, - "A DSLOAD surface must resolve after surface validation."); - return; - } - surface_pressure_loads.push_back( - {step_.name_raw, step_.name_canonical, - static_cast( - std::distance(definition_.element_surfaces.begin(), surface)), - load.surface_name, CanonicalizeAscii(load.surface_name), "P", - load.magnitude, load.magnitude_raw, load.keyword_location, - load.data_location, load.data_ordinal, load.load_ordinal}); - } - - // Static time-control values are provenance only: V0 creates exactly - // the canonical final frame 0 and performs no increment loop here. - definition_.steps.push_back( - {model_element_family_ == ElementFamily::kSolid ? step_.name_raw - : "Step-1", - std::move(boundaries), step_.loads, std::move(surface_pressure_loads), - step_.procedure_kind, step_.nlgeom, step_.static_values[0], - step_.static_values[1], step_.static_values[2], step_.static_values[3], - step_.location}); - definition_.steps.back().direct = step_.static_direct; - definition_.steps.back().increment_count = step_.static_increment_count; - definition_.steps.back().maximum_increments = step_.maximum_increments; - } - const ParsedInput& input_; ModelDefinition definition_{}; - std::optional failure_; - std::vector parts_; - std::vector instances_; - std::vector assembly_sets_; - std::vector element_surfaces_; - std::vector materials_; - std::vector model_boundaries_; - std::vector thermal_model_boundaries_; - std::vector thermal_initial_conditions_; - std::vector thermal_active_nodes_; - RawStep step_{}; - std::vector>> - part_section_assignments_; - std::vector>> - part_shell_section_assignments_; + TopologyCandidates topology_{*this}; std::optional current_part_; - std::optional pending_section_; - std::optional material_eligible_; std::optional model_element_family_; + MaterialPropertyCandidates material_properties_{*this, input_, + model_element_family_}; + SurfaceDefinitionCandidates surfaces_{*this, topology_, model_element_family_, + definition_}; + StepDefinitionCandidates steps_{*this, input_, model_element_family_, + definition_}; bool heading_seen_{false}; bool part_elements_seen_{false}; bool part_sets_seen_{false}; @@ -4591,12 +1238,6 @@ class DomainMappingContext::Impl { bool model_boundary_seen_{false}; bool in_assembly_{false}; bool in_instance_{false}; - bool step_seen_{false}; - bool in_step_{false}; - bool step_load_seen_{false}; - bool step_no_op_seen_{false}; - bool active_output_{false}; - bool active_output_is_field_{false}; std::optional physical_constants_location_; }; diff --git a/src/fesa/io/abaqus/input_reader.cpp b/src/fesa/io/abaqus/input_reader.cpp index 44ae217..a8410c7 100644 --- a/src/fesa/io/abaqus/input_reader.cpp +++ b/src/fesa/io/abaqus/input_reader.cpp @@ -12,6 +12,8 @@ #include #include +#include "fesa/core/ascii.h" + namespace fesa { namespace { @@ -40,16 +42,6 @@ std::string Trim(std::string_view text) { return std::string{text}; } -std::string UppercaseAscii(std::string value) { - std::transform(value.begin(), value.end(), value.begin(), [](char character) { - if (character >= 'a' && character <= 'z') { - return static_cast(character - 'a' + 'A'); - } - return character; - }); - return value; -} - std::vector SplitFields(std::string_view line) { std::vector fields; std::size_t field_start = 0U; @@ -88,13 +80,14 @@ bool AppendKeywordParameters(KeywordBlock& block, } const std::size_t equals = field.find('='); - const std::string parameter_name = + std::string parameter_name = Trim(std::string_view{field}.substr(0U, equals)); if (parameter_name.empty()) { return false; } - KeywordParameter parameter{UppercaseAscii(parameter_name), std::nullopt}; + KeywordParameter parameter{AsciiUppercase(std::move(parameter_name)), + std::nullopt}; if (equals != std::string::npos) { parameter.value = Trim(std::string_view{field}.substr(equals + 1U)); } @@ -132,8 +125,10 @@ Result Failure(const std::filesystem::path& source_path, std::move(keyword), "", std::move(message)}; + std::vector diagnostics; + diagnostics.push_back(std::move(diagnostic)); return Result::Failure( - Status::Failure(FailureCategory::kInput, {std::move(diagnostic)})); + Status::Failure(FailureCategory::kInput, std::move(diagnostics))); } } // namespace @@ -182,8 +177,9 @@ Result AbaqusInputReader::Read( } } + std::string trimmed_line = Trim(original_line); physical_lines.push_back( - {original_line, Trim(original_line), line_number}); + {std::move(original_line), std::move(trimmed_line), line_number}); if (newline == std::string::npos) { break; @@ -209,7 +205,7 @@ Result AbaqusInputReader::Read( "A keyword line requires a non-empty keyword name."); } - KeywordBlock block{UppercaseAscii(keyword_text), + KeywordBlock block{AsciiUppercase(keyword_text), line.original_line, {line.original_line}, {}, diff --git a/src/fesa/io/abaqus/mapping_diagnostics.h b/src/fesa/io/abaqus/mapping_diagnostics.h new file mode 100644 index 0000000..1a66a99 --- /dev/null +++ b/src/fesa/io/abaqus/mapping_diagnostics.h @@ -0,0 +1,105 @@ +#ifndef FESA_IO_ABAQUS_MAPPING_DIAGNOSTICS_H_ +#define FESA_IO_ABAQUS_MAPPING_DIAGNOSTICS_H_ + +#include +#include +#include +#include + +#include "fesa/core/status.h" +#include "fesa/io/abaqus/input_syntax.h" + +namespace fesa::abaqus_internal { + +struct MappingFailure { + FailureCategory category; + Diagnostic diagnostic; +}; + +/// @brief Drops only trailing empty syntax fields without changing interior fields. +std::vector WithoutTrailingEmpty(std::vector fields); + +/// @brief Retains the first mapping failure and shared lexical validation. +/// @note Mapping stage candidates borrow this context-owned service. +class MappingDiagnostics { + public: + /// @brief Reports whether a previous mapping stage retained a failure. + bool HasFailure() const noexcept { return failure_.has_value(); } + + /// @brief Reports the existing unsupported-keyword policy for this model + /// family. + void RejectUnknown(const KeywordBlock& block, bool shell_model); + + /// @brief Copies the retained first failure or returns successful mapping + /// status. + Status CurrentStatus() const; + + /// @brief Retains the first diagnostic of a failed nonempty stage status. + void RecordFailure(const Status& status); + + /// @brief Borrows one canonical parameter from its source keyword block. + const KeywordParameter* Parameter(const KeywordBlock& block, + std::string_view name) const; + + /// @brief Retains one diagnostic only if no earlier mapping failure exists. + bool Fail(FailureCategory category, std::string code, + const SourceLocation& location, std::string keyword, + std::string entity_identity, std::string message); + + /// @brief Retains the first input-category diagnostic. + bool InputFailure(std::string code, const SourceLocation& location, + std::string keyword, std::string entity_identity, + std::string message); + + /// @brief Retains the first model-category diagnostic. + bool ModelFailure(std::string code, const SourceLocation& location, + std::string keyword, std::string entity_identity, + std::string message); + + /// @brief Reports a keyword outside its approved source scope. + bool InvalidKeywordLocation(const KeywordBlock& block, std::string message); + + /// @brief Rejects duplicate or unsupported parameters in declaration order. + bool ValidateParameters( + const KeywordBlock& block, const std::vector& allowed, + std::string_view unsupported_code = "invalid-keyword-parameter"); + + /// @brief Borrows a required nonempty value from its source keyword block. + const std::string* RequiredParameterValue(const KeywordBlock& block, + std::string_view name); + + /// @brief Rejects the first source row of a data-free keyword. + bool RequireNoData(const KeywordBlock& block); + + /// @brief Parses the existing base-10 scalar policy with contextual + /// diagnostics. + bool ParseInteger(const std::string& text, std::int64_t& value, + const SourceLocation& location, const std::string& keyword, + bool positive); + + /// @brief Parses finite structural scalars with the existing input failure + /// policy. + bool ParseDouble(const std::string& text, double& value, + const SourceLocation& location, const std::string& keyword); + + /// @brief Parses thermal decimal scalars while preserving representable + /// subnormals. + bool ParseThermalDouble(const std::string& text, double& value, + const SourceLocation& location, + const std::string& keyword); + + /// @brief Distinguishes malformed syntax from nonfinite model property + /// values. + bool ParseModelDouble(const std::string& text, double& value, + const SourceLocation& location, + const std::string& keyword, + const std::string& diagnostic_code, + const std::string& message); + + protected: + std::optional failure_; +}; + +} // namespace fesa::abaqus_internal + +#endif // FESA_IO_ABAQUS_MAPPING_DIAGNOSTICS_H_ diff --git a/src/fesa/io/abaqus/material_property_candidates.h b/src/fesa/io/abaqus/material_property_candidates.h new file mode 100644 index 0000000..de7297b --- /dev/null +++ b/src/fesa/io/abaqus/material_property_candidates.h @@ -0,0 +1,185 @@ +#ifndef FESA_IO_ABAQUS_MATERIAL_PROPERTY_CANDIDATES_H_ +#define FESA_IO_ABAQUS_MATERIAL_PROPERTY_CANDIDATES_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fesa/model/model_types.h" +#include "io/abaqus/mapping_diagnostics.h" + +namespace fesa::abaqus_internal { + +enum class ElementFamily; +struct RawPart; + +struct RawSection { + std::string element_set_name; + std::string material_name; + std::array properties; + std::array first_axis; + std::vector> section_points; + SourceLocation location; +}; + +struct RawShellSection { + std::string element_set_name; + std::string material_name; + double thickness; + SourceLocation location; +}; + +struct RawSolidSection { + std::string element_set_name; + std::string material_name; + SourceLocation location; +}; + +/// @brief Owns source section candidates for one part before resolution. +struct PartPropertyCandidates { + std::vector sections; + std::vector shell_sections; + std::vector solid_sections; +}; + +/// @brief Supplies procedure policy without giving material mapping step state. +struct MaterialProcedureView { + struct Surface { + SourceLocation location; + std::string name; + }; + bool frequency; + bool dynamic; + const SourceLocation& location; + const std::string& name_raw; + std::optional first_surface; + const std::optional& physical_constants_location; +}; + +/// @brief Owns material candidates, their validation, and section assignments. +/// @note The context-owned diagnostics, input, and family outlive this object. +class MaterialPropertyCandidates { + public: + using Assignments = + std::map>; + + /// @brief Borrows context services while owning this uncommitted mapping + /// stage. + MaterialPropertyCandidates(MappingDiagnostics& diagnostics, + const ParsedInput& input, + const std::optional& family) + : diagnostics_{diagnostics}, + input_{input}, + model_element_family_{family} {} + + /// @brief Returns the source material declaration count. + std::size_t Count() const noexcept { return materials_.size(); } + /// @brief Reports whether no source material has been declared. + bool Empty() const noexcept { return materials_.empty(); } + /// @brief Reports whether a material child may follow the current + /// declaration. + bool HasActiveMaterial() const noexcept { + return material_eligible_.has_value(); + } + /// @brief Ends the optional section-point continuation scope. + void EndSection() noexcept { pending_section_.reset(); } + /// @brief Ends the material-child continuation scope. + void EndMaterial() noexcept { material_eligible_.reset(); } + /// @brief Borrows resolved beam section and material indices for one part. + const Assignments& BeamAssignments(std::size_t part) const { + return part_section_assignments_[part]; + } + /// @brief Borrows resolved shell or solid property and material indices for + /// one part. + const Assignments& ShellAssignments(std::size_t part) const { + return part_shell_section_assignments_[part]; + } + + /// @brief Parses material-only solid assignments without introducing + /// thickness. + void ParseSolidSection(const KeywordBlock& block, + PartPropertyCandidates& properties); + + /// @brief Parses centered shell thickness and source integration-point + /// metadata. + void ParseShellSection(const KeywordBlock& block, + PartPropertyCandidates& properties); + + /// @brief Parses general beam properties and their global guide vector. + void ParseBeamSection(const KeywordBlock& block, + PartPropertyCandidates& properties); + + /// @brief Parses optional beam stress points in the active section scope. + void ParseSectionPoints(const KeywordBlock& block, + PartPropertyCandidates& properties); + + /// @brief Starts one source material after syntax ordering has been checked. + void ParseMaterial(const KeywordBlock& block); + + /// @brief Parses an elastic row without conflating beam and shell material + /// ranges. + void ParseElastic(const KeywordBlock& block); + + /// @brief Parses positive finite density with its source-specific failure + /// policy. + void ParseDensity(const KeywordBlock& block); + + /// @brief Parses constant conductivity or specific heat in the thermal + /// subset. + void ParseThermalProperty(const KeywordBlock& block, bool conductivity); + + /// @brief Appends validated material definitions in source order. + void FinalizeMaterials(ModelDefinition& definition); + + /// @brief Validates part references and unique section/material assignments. + void FinalizePartsAndSections(const std::vector& parts, + ModelDefinition& definition); + + /// @brief Checks procedure policy before appending active material densities. + void FinalizeDensities(const MaterialProcedureView& procedure, + ModelDefinition& definition); + + private: + struct RawMaterial { + std::string name; + double youngs_modulus{0.0}; + double poisson_ratio{0.0}; + bool has_elastic{false}; + std::optional density; + std::optional conductivity; + std::optional specific_heat; + SourceLocation location; + SourceLocation elastic_location; + SourceLocation density_location; + }; + + bool ParseDensityValue(const std::string& text, double& value, + const SourceLocation& location); + + std::optional FindMaterialIndex(const std::string& name) const; + + void ValidateFrequencyPreprint(ModelDefinition& definition); + + void ValidateDynamicPreprint(); + + bool ContainsName(const std::vector& values, + const std::string& name) const; + + MappingDiagnostics& diagnostics_; + const ParsedInput& input_; + const std::optional& model_element_family_; + std::vector materials_; + std::optional material_eligible_; + std::optional pending_section_; + std::vector part_section_assignments_; + std::vector part_shell_section_assignments_; +}; + +} // namespace fesa::abaqus_internal + +#endif // FESA_IO_ABAQUS_MATERIAL_PROPERTY_CANDIDATES_H_ diff --git a/src/fesa/io/abaqus/material_property_mapper.cpp b/src/fesa/io/abaqus/material_property_mapper.cpp index 84cdc2b..82b4351 100644 --- a/src/fesa/io/abaqus/material_property_mapper.cpp +++ b/src/fesa/io/abaqus/material_property_mapper.cpp @@ -1,9 +1,927 @@ #include "io/abaqus/material_property_mapper.h" +#include +#include +#include +#include +#include + +#include "fesa/core/ascii.h" #include "io/abaqus/domain_mapping_context.h" +#include "io/abaqus/material_property_candidates.h" +#include "io/abaqus/topology_candidates.h" namespace fesa::abaqus_internal { +void MaterialPropertyCandidates::ParseMaterial(const KeywordBlock& block) { + if (!diagnostics_.ValidateParameters(block, {"NAME"}) || + !diagnostics_.RequireNoData(block)) { + return; + } + const auto* name = diagnostics_.RequiredParameterValue(block, "NAME"); + if (name == nullptr) { + return; + } + if (ContainsName(materials_, *name)) { + diagnostics_.InputFailure("duplicate-entity", block.location, + block.canonical_name, *name, + "Material names are globally unique."); + return; + } + materials_.push_back({*name, + 0.0, + 0.0, + false, + std::nullopt, + std::nullopt, + std::nullopt, + block.location, + {}, + {}}); + material_eligible_ = materials_.size() - 1U; +} + +void MaterialPropertyCandidates::ParseElastic(const KeywordBlock& block) { + if (model_element_family_ == ElementFamily::kThermalShell) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "ELASTIC is outside the thermal material subset."); + return; + } + if (!material_eligible_) { + diagnostics_.InputFailure("invalid-keyword-location", block.location, + block.canonical_name, "", + "ELASTIC must immediately follow MATERIAL."); + return; + } + if (!diagnostics_.ValidateParameters(block, {}) || block.data.size() != 1U || + block.data[0].fields.size() != 2U) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure("invalid-data-arity", block.location, + block.canonical_name, + materials_[*material_eligible_].name, + "ELASTIC requires exactly one E, nu row."); + } + return; + } + RawMaterial& material = materials_[*material_eligible_]; + if (material.has_elastic) { + diagnostics_.InputFailure("duplicate-entity", block.location, + block.canonical_name, material.name, + "A material accepts one ELASTIC definition."); + return; + } + if (!diagnostics_.ParseModelDouble( + block.data[0].fields[0], material.youngs_modulus, + block.data[0].location, block.canonical_name, + model_element_family_ == ElementFamily::kSolid + ? "invalid-solid-material" + : model_element_family_ == ElementFamily::kShell + ? "invalid-shell-material" + : "invalid-beam-property", + "Elastic material values must be finite.") || + !diagnostics_.ParseModelDouble( + block.data[0].fields[1], material.poisson_ratio, + block.data[0].location, block.canonical_name, + model_element_family_ == ElementFamily::kSolid + ? "invalid-solid-material" + : model_element_family_ == ElementFamily::kShell + ? "invalid-shell-material" + : "invalid-beam-property", + "Elastic material values must be finite.")) { + return; + } + material.has_elastic = true; + material.elastic_location = block.data[0].location; +} + +bool MaterialPropertyCandidates::ParseDensityValue( + const std::string& text, double& value, const SourceLocation& location) { + const std::string canonical = AsciiUppercase(text); + if (canonical == "NAN" || canonical == "+NAN" || canonical == "-NAN" || + canonical == "INF" || canonical == "+INF" || canonical == "-INF" || + canonical == "INFINITY" || canonical == "+INFINITY" || + canonical == "-INFINITY") { + return diagnostics_.ModelFailure( + "invalid-material-density", location, "DENSITY", text, + "Density must convert to a finite value greater than zero."); + } + std::size_t cursor = 0U; + if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { + ++cursor; + } + const std::size_t integer_start = cursor; + while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { + ++cursor; + } + const bool has_integer_digits = cursor != integer_start; + bool has_fraction_digits = false; + if (cursor < text.size() && text[cursor] == '.') { + ++cursor; + const std::size_t fraction_start = cursor; + while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { + ++cursor; + } + has_fraction_digits = cursor != fraction_start; + } + if (!has_integer_digits && !has_fraction_digits) { + return diagnostics_.InputFailure( + "invalid-density-data", location, "DENSITY", text, + "Density requires C-locale decimal syntax."); + } + if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) { + ++cursor; + if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { + ++cursor; + } + const std::size_t exponent_start = cursor; + while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { + ++cursor; + } + if (cursor == exponent_start) { + return diagnostics_.InputFailure( + "invalid-density-data", location, "DENSITY", text, + "Density exponents require one or more digits."); + } + } + if (cursor != text.size()) { + return diagnostics_.InputFailure( + "invalid-density-data", location, "DENSITY", text, + "Density requires C-locale decimal syntax."); + } + const char* begin = text.data(); + if (*begin == '+') { + ++begin; + } + double parsed = 0.0; + const auto conversion = std::from_chars(begin, text.data() + text.size(), + parsed, std::chars_format::general); + if (conversion.ec != std::errc{} || + conversion.ptr != text.data() + text.size() || !std::isfinite(parsed) || + !(parsed > 0.0)) { + return diagnostics_.ModelFailure( + "invalid-material-density", location, "DENSITY", text, + "Density must convert to a finite value greater than zero."); + } + value = parsed; + return true; +} + +void MaterialPropertyCandidates::ParseDensity(const KeywordBlock& block) { + if (!material_eligible_) { + diagnostics_.InputFailure("invalid-density-data", block.location, + block.canonical_name, "", + "DENSITY must be declared inside one MATERIAL."); + return; + } + if (!diagnostics_.ValidateParameters(block, {}, + "unsupported-density-option")) { + return; + } + RawMaterial& material = materials_[*material_eligible_]; + if (material.density) { + diagnostics_.InputFailure("duplicate-density", block.location, + block.canonical_name, material.name, + "A material accepts one DENSITY definition."); + return; + } + if (block.data.size() != 1U || + (block.data[0].fields.size() != 1U && + (block.data[0].fields.size() != 2U || + !block.data[0].fields[1].empty())) || + block.data[0].fields[0].empty()) { + diagnostics_.InputFailure("invalid-density-data", block.location, + block.canonical_name, material.name, + "DENSITY requires exactly rho or rho,."); + return; + } + double rho = 0.0; + if (!ParseDensityValue(block.data[0].fields[0], rho, + block.data[0].location)) { + return; + } + material.density = rho; + material.density_location = block.data[0].location; +} + +void MaterialPropertyCandidates::ParseThermalProperty(const KeywordBlock& block, + bool conductivity) { + if (model_element_family_ != ElementFamily::kThermalShell || + !material_eligible_ || !diagnostics_.ValidateParameters(block, {})) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure("invalid-keyword-location", block.location, + block.canonical_name, "", + "Thermal properties belong to a DS4 material."); + } + return; + } + RawMaterial& material = materials_[*material_eligible_]; + std::optional& destination = + conductivity ? material.conductivity : material.specific_heat; + if (destination || block.data.size() != 1U || + (block.data[0].fields.size() != 1U && + (block.data[0].fields.size() != 2U || + !block.data[0].fields[1].empty()))) { + diagnostics_.InputFailure("invalid-data-arity", block.location, + block.canonical_name, material.name, + "Thermal property requires one constant row."); + return; + } + double value = 0.0; + if (!diagnostics_.ParseThermalDouble(block.data[0].fields[0], value, + block.data[0].location, + block.canonical_name) || + !(value > 0.0)) { + if (!diagnostics_.HasFailure()) { + diagnostics_.ModelFailure( + "invalid-thermal-material", block.data[0].location, + block.canonical_name, material.name, + "Thermal material values must be finite and positive."); + } + return; + } + destination = value; +} + +std::optional MaterialPropertyCandidates::FindMaterialIndex( + const std::string& name) const { + for (std::size_t index = 0U; index < materials_.size(); ++index) { + if (AsciiCaseInsensitiveEquals(materials_[index].name, name)) { + return static_cast(index); + } + } + return std::nullopt; +} + +void MaterialPropertyCandidates::FinalizeMaterials( + ModelDefinition& definition) { + for (const auto& material : materials_) { + if (model_element_family_ == ElementFamily::kThermalShell) { + if (!material.density || !material.conductivity || + !material.specific_heat) { + diagnostics_.ModelFailure( + "missing-thermal-material-property", material.location, "MATERIAL", + material.name, + "DS4 materials require DENSITY, CONDUCTIVITY, and " + "SPECIFIC HEAT."); + return; + } + definition.thermal_materials.push_back({{"", 0, material.name}, + material.name, + *material.conductivity, + *material.density, + *material.specific_heat, + material.location}); + continue; + } + if (!material.has_elastic) { + diagnostics_.InputFailure("unresolved-reference", material.location, + "MATERIAL", material.name, + "A material must own exactly one ELASTIC row."); + return; + } + if (model_element_family_ == ElementFamily::kShell || + model_element_family_ == ElementFamily::kSolid) { + if (!(material.youngs_modulus > 0.0) || + !(material.poisson_ratio > -1.0) || !(material.poisson_ratio < 0.5)) { + diagnostics_.ModelFailure( + model_element_family_ == ElementFamily::kSolid + ? "invalid-solid-material" + : "invalid-shell-material", + material.elastic_location, "ELASTIC", material.name, + "Isotropic elasticity requires E>0 and -1 0.0) || !std::isfinite(shear_modulus) || + !(shear_modulus > 0.0)) { + diagnostics_.ModelFailure( + "invalid-beam-property", material.elastic_location, "ELASTIC", + material.name, "E and the derived G=E/(2*(1+nu)) must be positive."); + return; + } + definition.materials.push_back({material.name, material.youngs_modulus, + material.poisson_ratio, material.location}); + } +} + +void MaterialPropertyCandidates::FinalizeDensities( + const MaterialProcedureView& procedure, ModelDefinition& definition) { + if (model_element_family_ == ElementFamily::kThermalShell) { + for (const auto& block : input_.blocks) { + if (block.canonical_name != "PREPRINT") continue; + if (!diagnostics_.ValidateParameters( + block, {"ECHO", "MODEL", "HISTORY", "CONTACT"})) + return; + if (block.parameters.size() != 4U) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Thermal PREPRINT requires four NO parameters."); + return; + } + for (const auto& parameter : block.parameters) { + if (!parameter.value || + !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, + parameter.name, "Thermal PREPRINT parameters must equal NO."); + return; + } + } + } + return; + } + if (procedure.physical_constants_location) { + diagnostics_.InputFailure( + "unsupported-keyword", *procedure.physical_constants_location, + "PHYSICAL CONSTANTS", "", + "PHYSICAL CONSTANTS is supported only by thermal input."); + return; + } + if (!procedure.frequency && !procedure.dynamic && + model_element_family_ == ElementFamily::kSolid) { + for (std::size_t index = 0U; index < materials_.size(); ++index) { + if (materials_[index].density) + definition.densities.push_back({static_cast(index), + *materials_[index].density, + materials_[index].density_location}); + } + return; + } + if (!procedure.frequency && !procedure.dynamic) { + if (std::any_of(materials_.begin(), materials_.end(), + [](const RawMaterial& material) { + return material.density.has_value(); + })) { + diagnostics_.InputFailure( + "invalid-density-data", procedure.location, "DENSITY", "", + "DENSITY is supported only by the FREQUENCY procedure."); + } + return; + } + if (procedure.frequency) { + ValidateFrequencyPreprint(definition); + if (diagnostics_.HasFailure()) { + return; + } + } + if (procedure.dynamic) { + ValidateDynamicPreprint(); + if (diagnostics_.HasFailure()) { + return; + } + for (auto& warning : definition.warnings) { + if (warning.keyword == "PREPRINT") { + warning.code = "ignored-output-request"; + } + } + } + if (model_element_family_ != ElementFamily::kShell && + model_element_family_ != ElementFamily::kSolid) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", procedure.location, + procedure.frequency ? "FREQUENCY" : "DYNAMIC", procedure.name_raw, + "The procedure requires an approved Shell or Solid model."); + return; + } + if (procedure.first_surface.has_value()) { + diagnostics_.InputFailure( + "unsupported-keyword", procedure.first_surface->location, "SURFACE", + procedure.first_surface->name, + "The selected procedure does not support element surfaces."); + return; + } + for (std::size_t index = 0U; index < materials_.size(); ++index) { + const RawMaterial& material = materials_[index]; + if (!material.density) { + bool active = false; + for (const auto& section : definition.shell_sections) { + if (section.material_index == static_cast(index)) { + active = true; + break; + } + } + for (const auto& section : definition.solid_sections) { + if (section.MaterialIndex() == static_cast(index)) + active = true; + } + if (active) { + diagnostics_.ModelFailure( + "missing-material-density", material.location, "MATERIAL", + material.name, + "Each active dynamic or frequency material requires DENSITY."); + return; + } + continue; + } + definition.densities.push_back({static_cast(index), + *material.density, + material.density_location}); + } +} + +void MaterialPropertyCandidates::ValidateFrequencyPreprint( + ModelDefinition& definition) { + for (const auto& block : input_.blocks) { + if (block.canonical_name != "PREPRINT") { + continue; + } + if (!diagnostics_.ValidateParameters( + block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) || + block.parameters.size() != 4U) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "FREQUENCY requires PREPRINT with ECHO, MODEL, HISTORY, " + "and CONTACT parameters."); + } + return; + } + for (const auto& parameter : block.parameters) { + if (!parameter.value || + !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, + parameter.name, + "FREQUENCY requires PREPRINT parameters to equal NO."); + return; + } + } + } + for (auto& warning : definition.warnings) { + if (warning.keyword == "PREPRINT") { + warning.message = "mandatory-eigen-output-unchanged"; + } + } +} + +void MaterialPropertyCandidates::ValidateDynamicPreprint() { + for (const auto& block : input_.blocks) { + if (block.canonical_name != "PREPRINT") { + continue; + } + if (!diagnostics_.ValidateParameters( + block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) || + block.parameters.size() != 4U) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "DYNAMIC requires PREPRINT with ECHO, MODEL, HISTORY, " + "and CONTACT parameters."); + } + return; + } + for (const auto& parameter : block.parameters) { + if (!parameter.value || + !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, + parameter.name, + "DYNAMIC requires PREPRINT parameters to equal NO."); + return; + } + } + } +} + +void MaterialPropertyCandidates::FinalizePartsAndSections( + const std::vector& parts, ModelDefinition& definition) { + part_section_assignments_.resize(parts.size()); + part_shell_section_assignments_.resize(parts.size()); + const bool shell_model = + model_element_family_ == ElementFamily::kShell || + model_element_family_ == ElementFamily::kThermalShell; + for (std::size_t part_index = 0U; part_index < parts.size(); ++part_index) { + const RawPart& part = parts[part_index]; + if (part.nodes.empty() || part.elements.empty() || + (!shell_model && model_element_family_ != ElementFamily::kSolid && + part.properties.sections.empty())) { + diagnostics_.InputFailure( + "invalid-model-cardinality", part.location, "PART", part.name, + "A part requires nodes, elements, and its approved " + "section form."); + return; + } + + PartDefinition part_definition{}; + part_definition.name = part.name; + part_definition.location = part.location; + for (const auto& node : part.nodes) { + part_definition.node_source_labels.push_back(node.label); + } + for (const auto& element : part.elements) { + part_definition.element_source_labels.push_back(element.label); + for (const auto node_label : element.node_labels) { + if (FindNode(part, node_label) == nullptr) { + diagnostics_.InputFailure( + shell_model ? "invalid-shell-connectivity" + : "unresolved-reference", + element.location, "ELEMENT", element.label_text, + "Element connectivity must resolve within its part."); + return; + } + } + } + for (const auto& set : part.node_sets) { + part_definition.node_set_names.push_back(set.name); + for (const auto label : set.members) { + if (FindNode(part, label) == nullptr) { + diagnostics_.InputFailure( + "unresolved-reference", set.location, "NSET", set.name, + "Every node-set member must resolve within its part."); + return; + } + } + } + for (const auto& set : part.element_sets) { + part_definition.element_set_names.push_back(set.name); + for (const auto label : set.members) { + if (FindElement(part, label) == nullptr) { + diagnostics_.InputFailure( + "unresolved-reference", set.location, "ELSET", set.name, + "Every element-set member must resolve within its part."); + return; + } + } + } + definition.parts.push_back(std::move(part_definition)); + + if (model_element_family_ == ElementFamily::kSolid) { + auto& assignments = part_shell_section_assignments_[part_index]; + for (const auto& section : part.properties.solid_sections) { + const auto* element_set = + FindSet(part.element_sets, section.element_set_name); + const auto material = FindMaterialIndex(section.material_name); + if (!element_set || !material) { + diagnostics_.InputFailure("unresolved-solid-section", + section.location, "SOLID SECTION", + section.element_set_name, + "Solid ELSET and MATERIAL must resolve."); + return; + } + const auto index = + static_cast(definition.solid_sections.size()); + definition.solid_sections.emplace_back( + SourceEntityId{part.name, static_cast(index), + section.element_set_name}, + section.element_set_name, *material, section.location); + for (const auto label : element_set->members) { + if (!assignments.emplace(label, std::make_pair(index, *material)) + .second) { + diagnostics_.InputFailure( + "invalid-solid-section-assignment", section.location, + "SOLID SECTION", std::to_string(label), + "Solid elements require exactly one section assignment."); + return; + } + } + } + for (const auto& element : part.elements) { + if (assignments.find(element.label) == assignments.end()) { + diagnostics_.InputFailure( + "invalid-solid-section-assignment", element.location, "ELEMENT", + element.label_text, "A Solid section assignment is missing."); + return; + } + } + continue; + } + if (shell_model) { + auto& assignments = part_shell_section_assignments_[part_index]; + for (const auto& section : part.properties.shell_sections) { + const auto* element_set = + FindSet(part.element_sets, section.element_set_name); + const auto material_index = FindMaterialIndex(section.material_name); + if (element_set == nullptr || !material_index) { + diagnostics_.InputFailure( + "unresolved-shell-section", section.location, "SHELL SECTION", + section.element_set_name, + "Shell section ELSET and MATERIAL references " + "must resolve."); + return; + } + const EntityIndex section_index = + model_element_family_ == ElementFamily::kThermalShell + ? static_cast(definition.thermal_sections.size()) + : static_cast(definition.shell_sections.size()); + if (model_element_family_ == ElementFamily::kThermalShell) { + definition.thermal_sections.push_back( + {{part.name, static_cast(section_index), + section.element_set_name}, + section.element_set_name, + section.thickness, + *material_index, + section.location}); + } else { + definition.shell_sections.push_back( + {section.element_set_name, section.thickness, *material_index, + section.location}); + } + for (const auto element_label : element_set->members) { + if (!assignments + .emplace(element_label, + std::make_pair(section_index, *material_index)) + .second) { + diagnostics_.InputFailure("invalid-shell-section-assignment", + section.location, "SHELL SECTION", + std::to_string(element_label), + "A shell element cannot receive multiple " + "section assignments."); + return; + } + } + } + for (const auto& element : part.elements) { + if (assignments.find(element.label) == assignments.end()) { + diagnostics_.InputFailure("invalid-shell-section-assignment", + element.location, "ELEMENT", + element.label_text, + "Every shell element requires exactly one " + "resolved section assignment."); + return; + } + } + continue; + } + + auto& assignments = part_section_assignments_[part_index]; + for (const auto& section : part.properties.sections) { + const auto* element_set = + FindSet(part.element_sets, section.element_set_name); + const auto material_index = FindMaterialIndex(section.material_name); + if (element_set == nullptr || !material_index) { + diagnostics_.InputFailure( + "unresolved-reference", section.location, "BEAM GENERAL SECTION", + section.element_set_name, + "Section ELSET and MATERIAL references must resolve."); + return; + } + if (section.properties[2] != 0.0) { + diagnostics_.ModelFailure("unsupported-coupled-section", + section.location, "BEAM GENERAL SECTION", + section.element_set_name, + "V0 requires exact I12=0."); + return; + } + if (!(section.properties[0] > 0.0) || !(section.properties[1] > 0.0) || + !(section.properties[3] > 0.0) || !(section.properties[4] > 0.0)) { + diagnostics_.ModelFailure( + "invalid-beam-property", section.location, "BEAM GENERAL SECTION", + section.element_set_name, "A, I11, I22, and J must be positive."); + return; + } + const EntityIndex section_index = + static_cast(definition.sections.size()); + definition.sections.push_back( + {section.element_set_name, section.properties[0], + section.properties[1], section.properties[2], section.properties[3], + section.properties[4], section.first_axis, section.section_points, + section.location}); + for (const auto element_label : element_set->members) { + if (!assignments + .emplace(element_label, + std::make_pair(section_index, *material_index)) + .second) { + diagnostics_.InputFailure( + "duplicate-entity", section.location, "BEAM GENERAL SECTION", + std::to_string(element_label), + "An element cannot receive multiple section " + "assignments."); + return; + } + } + } + for (const auto& element : part.elements) { + if (assignments.find(element.label) == assignments.end()) { + diagnostics_.InputFailure( + "unresolved-reference", element.location, "ELEMENT", + element.label_text, + "Every B33 element requires a resolved section assignment."); + return; + } + } + } +} + +bool MaterialPropertyCandidates::ContainsName( + const std::vector& values, const std::string& name) const { + return std::any_of(values.begin(), values.end(), + [&name](const RawMaterial& value) { + return AsciiCaseInsensitiveEquals(value.name, name); + }); +} + +void MaterialPropertyCandidates::ParseSolidSection( + const KeywordBlock& block, PartPropertyCandidates& properties) { + if (!diagnostics_.ValidateParameters(block, {"ELSET", "MATERIAL"})) return; + const auto* elset = diagnostics_.RequiredParameterValue(block, "ELSET"); + const auto* material = diagnostics_.RequiredParameterValue(block, "MATERIAL"); + if (!elset || !material) return; + if (block.data.size() > 1U || + (!block.data.empty() && + std::any_of(block.data[0].fields.begin(), block.data[0].fields.end(), + [](const auto& field) { return !field.empty(); }))) { + diagnostics_.InputFailure( + "invalid-solid-section", block.location, block.canonical_name, *elset, + "SOLID SECTION permits omitted or blank data only."); + return; + } + properties.solid_sections.push_back({*elset, *material, block.location}); +} + +void MaterialPropertyCandidates::ParseShellSection( + const KeywordBlock& block, PartPropertyCandidates& properties) { + for (const auto& candidate : block.parameters) { + if (candidate.name != "ELSET" && candidate.name != "MATERIAL") { + diagnostics_.InputFailure( + "unsupported-shell-section-option", block.location, + block.canonical_name, candidate.name, + "The shell-section option is outside the centered " + "single-layer subset."); + return; + } + } + if (!diagnostics_.ValidateParameters(block, {"ELSET", "MATERIAL"})) { + return; + } + const auto* element_set = diagnostics_.RequiredParameterValue(block, "ELSET"); + const auto* material = diagnostics_.RequiredParameterValue(block, "MATERIAL"); + if (element_set == nullptr || material == nullptr) { + return; + } + if (block.data.size() != 1U || block.data[0].fields.empty() || + block.data[0].fields.size() > 2U) { + diagnostics_.InputFailure( + "unsupported-shell-section-option", block.location, + block.canonical_name, *element_set, + "SHELL SECTION requires one thickness row with one optional " + "integration-point field."); + return; + } + double thickness = 0.0; + const bool parsed_thickness = + model_element_family_ == ElementFamily::kThermalShell + ? diagnostics_.ParseThermalDouble(block.data[0].fields[0], thickness, + block.data[0].location, + block.canonical_name) + : diagnostics_.ParseModelDouble( + block.data[0].fields[0], thickness, block.data[0].location, + block.canonical_name, "invalid-shell-thickness", + "Shell thickness must be finite and positive."); + if (!parsed_thickness) { + return; + } + if (!(thickness > 0.0)) { + diagnostics_.ModelFailure("invalid-shell-thickness", block.data[0].location, + block.canonical_name, *element_set, + "Shell thickness must be finite and positive."); + return; + } + if (block.data[0].fields.size() == 2U) { + const auto integration_points = + ParsePositiveSourceLabel(block.data[0].fields[1]); + if (!integration_points.HasValue()) { + diagnostics_.InputFailure( + "unsupported-shell-section-option", block.data[0].location, + block.canonical_name, block.data[0].fields[1], + "The optional shell integration-point field must be a " + "positive integer."); + return; + } + if (model_element_family_ == ElementFamily::kThermalShell && + integration_points.Value() != 5) { + diagnostics_.InputFailure( + "unsupported-shell-section-option", block.data[0].location, + block.canonical_name, block.data[0].fields[1], + "Thermal DS4 sections require exactly five integration points."); + return; + } + } + properties.shell_sections.push_back( + {*element_set, *material, thickness, block.location}); +} + +void MaterialPropertyCandidates::ParseBeamSection( + const KeywordBlock& block, PartPropertyCandidates& properties) { + pending_section_.reset(); + if (!diagnostics_.ValidateParameters(block, + {"ELSET", "MATERIAL", "SECTION"})) { + return; + } + const auto* element_set = diagnostics_.RequiredParameterValue(block, "ELSET"); + const auto* material = diagnostics_.RequiredParameterValue(block, "MATERIAL"); + const auto* section = diagnostics_.RequiredParameterValue(block, "SECTION"); + if (element_set == nullptr || material == nullptr || section == nullptr) { + return; + } + if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) { + diagnostics_.InputFailure( + "unsupported-section-formulation", block.location, block.canonical_name, + *section, "Only SECTION=GENERAL belongs to the approved subset."); + return; + } + if (std::any_of(properties.sections.begin(), properties.sections.end(), + [element_set](const RawSection& existing) { + return AsciiCaseInsensitiveEquals(existing.element_set_name, + *element_set); + })) { + diagnostics_.InputFailure( + "duplicate-entity", block.location, block.canonical_name, *element_set, + "An element set can receive only one beam section."); + return; + } + if (block.data.size() != 2U || block.data[0].fields.size() != 5U || + block.data[1].fields.size() != 3U) { + diagnostics_.InputFailure( + "invalid-data-arity", block.location, block.canonical_name, + *element_set, + "A general section requires one five-field property row and " + "one three-field axis row."); + return; + } + RawSection raw{*element_set, *material, {}, {}, {}, block.location}; + for (std::size_t property = 0U; property < 5U; ++property) { + if (!diagnostics_.ParseModelDouble( + block.data[0].fields[property], raw.properties[property], + block.data[0].location, block.canonical_name, + "invalid-beam-property", + "Beam section properties must be finite.")) { + return; + } + } + for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { + if (!diagnostics_.ParseModelDouble( + block.data[1].fields[coordinate], raw.first_axis[coordinate], + block.data[1].location, block.canonical_name, + "invalid-beam-guide-vector", + "The beam guide vector must be finite.")) { + return; + } + } + properties.sections.push_back(std::move(raw)); + pending_section_ = properties.sections.size() - 1U; +} + +void MaterialPropertyCandidates::ParseSectionPoints( + const KeywordBlock& block, PartPropertyCandidates& properties) { + if (!pending_section_ || !diagnostics_.ValidateParameters(block, {}) || + block.data.empty()) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "invalid-keyword-location", block.location, block.canonical_name, "", + "SECTION POINTS must immediately follow a general section."); + } + return; + } + RawSection& section = properties.sections[*pending_section_]; + for (const auto& row : block.data) { + if (row.fields.size() != 2U) { + diagnostics_.InputFailure("invalid-data-arity", row.location, + block.canonical_name, section.element_set_name, + "Section-point rows require x1 and x2."); + return; + } + std::array point{}; + if (!diagnostics_.ParseDouble(row.fields[0], point[0], row.location, + block.canonical_name) || + !diagnostics_.ParseDouble(row.fields[1], point[1], row.location, + block.canonical_name)) { + return; + } + if (std::find(section.section_points.begin(), section.section_points.end(), + point) != section.section_points.end()) { + diagnostics_.InputFailure( + "duplicate-entity", row.location, block.canonical_name, + section.element_set_name, + "Section points must be unique within a section."); + return; + } + section.section_points.push_back(point); + } + pending_section_.reset(); +} + Status MaterialPropertyMapper::Map(DomainMappingContext& context) const { return context.FinalizeMaterialProperties(); } diff --git a/src/fesa/io/abaqus/step_definition_candidates.h b/src/fesa/io/abaqus/step_definition_candidates.h new file mode 100644 index 0000000..da5d9d7 --- /dev/null +++ b/src/fesa/io/abaqus/step_definition_candidates.h @@ -0,0 +1,196 @@ +#ifndef FESA_IO_ABAQUS_STEP_DEFINITION_CANDIDATES_H_ +#define FESA_IO_ABAQUS_STEP_DEFINITION_CANDIDATES_H_ + +#include +#include +#include +#include +#include +#include + +#include "fesa/model/source_target_resolver.h" +#include "io/abaqus/topology_candidates.h" + +namespace fesa::abaqus_internal { + +struct DomainMappingCandidateSummary; + +/// @brief Owns uncommitted step, boundary, load, and initial-condition syntax. +/// @note The context-owned input, family, diagnostics, and candidate outlive +/// it. +class StepDefinitionCandidates { + public: + /// @brief Borrows context services while owning this uncommitted mapping + /// stage. + StepDefinitionCandidates(MappingDiagnostics& diagnostics, + const ParsedInput& input, + const std::optional& family, + ModelDefinition& definition) + : diagnostics_{diagnostics}, + input_{input}, + model_element_family_{family}, + definition_{definition} {} + + /// @brief Reports whether the sole source step has been declared. + bool HasStep() const noexcept { return step_seen_; } + /// @brief Reports whether source step syntax remains open. + bool InStep() const noexcept { return in_step_; } + /// @brief Reports whether the step selected an existing supported procedure. + bool HasSupportedProcedure() const noexcept; + /// @brief Ends output-child scope when source syntax routing changes. + void EndOutput() noexcept { active_output_ = false; } + /// @brief Copies stable candidate counts without committing a Domain. + void FillSummary(DomainMappingCandidateSummary& summary) const noexcept; + /// @brief Borrows procedure provenance for immediate material validation. + MaterialProcedureView MaterialProcedure( + std::optional first_surface, + const std::optional& physical_constants) const; + /// @brief Owns model-level structural or thermal boundary rows. + void ParseModelBoundary(const KeywordBlock& block); + + /// @brief Parses step controls while preserving repeated-step first failure. + void ParseStepStart(const KeywordBlock& block, bool model_ready); + + /// @brief Parses children with the existing procedure allowlist and ordering. + void ParseStepBlock(const KeywordBlock& block); + + /// @brief Owns supported model-level load amplitude source records. + void ParseAmplitude(const KeywordBlock& block); + + /// @brief Owns supported thermal initial-temperature source rows. + void ParseInitialConditions(const KeywordBlock& block); + + /// @brief Appends source no-op provenance with its procedure warning code. + void AddIgnoredWarning(const KeywordBlock& block); + + /// @brief Resolves source targets and appends complete step definitions. + void FinalizeStep(); + + private: + struct RawSurfacePressureLoad { + std::string surface_name; + double magnitude; + std::string magnitude_raw; + SourceLocation keyword_location; + SourceLocation data_location; + std::size_t data_ordinal; + std::size_t load_ordinal; + }; + + struct RawThermalBoundary { + std::string target; + int dof; + double value; + SourceLocation location; + }; + + struct RawThermalInitial { + std::string target; + double value; + SourceLocation location; + }; + + struct RawThermalFlux { + std::string target; + int dof; + double power; + SourceLocation location; + }; + + struct RawStep { + SourceLocation location; + std::string name_raw; + std::string name_canonical; + bool has_static{false}; + bool static_direct{false}; + std::uint32_t static_increment_count{0U}; + std::optional dynamic; + bool perturbation_explicit{false}; + bool nlgeom_explicit{false}; + StaticProcedureKind procedure_kind{ + StaticProcedureKind::kLinearSmallDisplacement}; + bool nlgeom{false}; + std::uint32_t maximum_increments{0U}; + std::array static_values{}; + std::optional frequency; + bool has_heat_transfer{false}; + double thermal_increment{0.0}; + double thermal_period{0.0}; + std::vector thermal_boundaries; + std::vector thermal_fluxes; + std::vector boundaries; + std::vector loads; + std::vector surface_pressure_loads; + std::size_t next_load_ordinal{0U}; + }; + + bool ParseSurfacePressureDouble(const std::string& text, double& value, + const SourceLocation& location, + const std::string& keyword); + + bool ParseNonlinearStaticDouble(const std::string& text, double& value, + const SourceLocation& location, + const std::string& keyword); + + bool ParseDynamicDouble(const std::string& text, double& value, + const SourceLocation& location, + const std::string& keyword); + + void ParseThermalBoundary(const KeywordBlock& block, + std::vector& destination); + + void ParseBoundary( + const KeywordBlock& block, + std::vector& destination); + + void ParseCload(const KeywordBlock& block, std::vector& loads); + + void ParseDsload(const KeywordBlock& block); + + bool ParseFrequencyModeCount(const std::string& text, std::int64_t& value, + const SourceLocation& location); + + void ParseFrequency(const KeywordBlock& block); + + void ValidateEigenBoundaries( + const std::vector& boundaries); + + void ValidateDynamicBoundaries( + const std::vector& boundaries); + + void ParseDynamic(const KeywordBlock& block); + + void ParseHeatTransfer(const KeywordBlock& block); + + void ParseCflux(const KeywordBlock& block); + + void ParseStatic(const KeywordBlock& block); + + void ParseOutputRoot(const KeywordBlock& block); + + void ParseOutputChild(const KeywordBlock& block); + + std::optional> ResolveNodeTarget( + const SourceTargetResolver& resolver, const std::string& target, + const SourceLocation& location, const std::string& keyword); + + MappingDiagnostics& diagnostics_; + const ParsedInput& input_; + const std::optional& model_element_family_; + ModelDefinition& definition_; + RawStep step_{}; + std::vector model_boundaries_; + std::vector thermal_model_boundaries_; + std::vector thermal_initial_conditions_; + std::vector thermal_active_nodes_; + bool step_seen_{false}; + bool in_step_{false}; + bool step_load_seen_{false}; + bool step_no_op_seen_{false}; + bool active_output_{false}; + bool active_output_is_field_{false}; +}; + +} // namespace fesa::abaqus_internal + +#endif // FESA_IO_ABAQUS_STEP_DEFINITION_CANDIDATES_H_ diff --git a/src/fesa/io/abaqus/step_definition_mapper.cpp b/src/fesa/io/abaqus/step_definition_mapper.cpp index 2ecd82d..d543951 100644 --- a/src/fesa/io/abaqus/step_definition_mapper.cpp +++ b/src/fesa/io/abaqus/step_definition_mapper.cpp @@ -1,9 +1,1679 @@ #include "io/abaqus/step_definition_mapper.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fesa/analysis/thermal_time_grid.h" +#include "fesa/core/ascii.h" #include "io/abaqus/domain_mapping_context.h" +#include "io/abaqus/step_definition_candidates.h" +#include "io/abaqus/surface_definition_candidates.h" namespace fesa::abaqus_internal { +bool StepDefinitionCandidates::HasSupportedProcedure() const noexcept { + return step_.has_static || step_.frequency || step_.dynamic || + step_.has_heat_transfer; +} + +void StepDefinitionCandidates::FillSummary( + DomainMappingCandidateSummary& summary) const noexcept { + summary.model_boundary_count = model_boundaries_.size(); + summary.step_boundary_count = step_.boundaries.size(); + summary.step_load_count = step_.loads.size(); + summary.has_static_step = step_.has_static; +} + +MaterialProcedureView StepDefinitionCandidates::MaterialProcedure( + std::optional first_surface, + const std::optional& physical_constants) const { + return {step_.frequency.has_value(), + step_.dynamic.has_value(), + step_.location, + step_.name_raw, + std::move(first_surface), + physical_constants}; +} + +void StepDefinitionCandidates::ParseModelBoundary(const KeywordBlock& block) { + if (model_element_family_ == ElementFamily::kThermalShell) { + ParseThermalBoundary(block, thermal_model_boundaries_); + } else { + ParseBoundary(block, model_boundaries_); + } +} + +bool StepDefinitionCandidates::ParseSurfacePressureDouble( + const std::string& text, double& value, const SourceLocation& location, + const std::string& keyword) { + std::size_t cursor = 0U; + if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { + ++cursor; + } + const std::size_t integer_start = cursor; + while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { + ++cursor; + } + const bool has_integer_digits = cursor != integer_start; + bool has_fraction_digits = false; + if (cursor < text.size() && text[cursor] == '.') { + ++cursor; + const std::size_t fraction_start = cursor; + while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { + ++cursor; + } + has_fraction_digits = cursor != fraction_start; + } + if (!has_integer_digits && !has_fraction_digits) { + return diagnostics_.InputFailure( + "invalid-numeric-value", location, keyword, text, + "Pressure values require C-locale decimal syntax."); + } + if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) { + ++cursor; + if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { + ++cursor; + } + const std::size_t exponent_start = cursor; + while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { + ++cursor; + } + if (cursor == exponent_start) { + return diagnostics_.InputFailure( + "invalid-numeric-value", location, keyword, text, + "Pressure exponents require one or more digits."); + } + } + if (cursor != text.size()) { + return diagnostics_.InputFailure( + "invalid-numeric-value", location, keyword, text, + "Pressure values require C-locale decimal syntax."); + } + const char* const begin = text.data(); + const char* const end = begin + text.size(); + const char* const numeric_begin = *begin == '+' ? begin + 1U : begin; + const auto parsed = + std::from_chars(numeric_begin, end, value, std::chars_format::general); + if (parsed.ec != std::errc{} || parsed.ptr != end || !std::isfinite(value)) { + return diagnostics_.InputFailure( + "invalid-numeric-value", location, keyword, text, + "Pressure values must be finite C-locale values."); + } + return true; +} + +bool StepDefinitionCandidates::ParseNonlinearStaticDouble( + const std::string& text, double& value, const SourceLocation& location, + const std::string& keyword) { + if (text.empty()) { + return diagnostics_.InputFailure( + "invalid-nonlinear-static-data", location, keyword, text, + "A nonlinear STATIC field cannot be empty."); + } + char* end = nullptr; + errno = 0; + const double parsed = std::strtod(text.c_str(), &end); + if (errno == ERANGE || end != text.c_str() + text.size() || + !std::isfinite(parsed)) { + return diagnostics_.InputFailure( + "invalid-nonlinear-static-data", location, keyword, text, + "All four nonlinear STATIC fields must be finite."); + } + value = parsed; + return true; +} + +bool StepDefinitionCandidates::ParseDynamicDouble( + const std::string& text, double& value, const SourceLocation& location, + const std::string& keyword) { + if (text.empty()) { + return diagnostics_.InputFailure("invalid-dynamic-data", location, keyword, + text, + "Dynamic data fields cannot be empty."); + } + char* end = nullptr; + errno = 0; + const double parsed = std::strtod(text.c_str(), &end); + if (errno == ERANGE || end != text.c_str() + text.size() || + !std::isfinite(parsed)) { + return diagnostics_.InputFailure("invalid-dynamic-data", location, keyword, + text, + "Dynamic data fields must be finite."); + } + value = parsed; + return true; +} + +void StepDefinitionCandidates::ParseAmplitude(const KeywordBlock& block) { + const bool dynamic_source = std::any_of( + input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) { + return candidate.canonical_name == "DYNAMIC"; + }); + if (model_element_family_ != ElementFamily::kSolid || !dynamic_source) { + diagnostics_.InputFailure( + "unsupported-load-amplitude", block.location, block.canonical_name, "", + "Amplitude is supported only in Solid dynamic models."); + return; + } + if (!diagnostics_.ValidateParameters(block, + {"NAME", "DEFINITION", "TIME", "VALUE"})) + return; + const auto* name = diagnostics_.RequiredParameterValue(block, "NAME"); + if (!name) return; + for (const auto& option : std::array, 3>{ + {{"DEFINITION", "TABULAR"}, + {"TIME", "STEP TIME"}, + {"VALUE", "RELATIVE"}}}) { + const auto* parameter = diagnostics_.Parameter(block, option.first); + if (parameter && + (!parameter->value || + !AsciiCaseInsensitiveEquals(*parameter->value, option.second))) { + diagnostics_.InputFailure( + "unsupported-amplitude-option", block.location, block.canonical_name, + option.first, + "Only TABULAR STEP TIME RELATIVE amplitude is supported."); + return; + } + } + const std::string canonical = AsciiUppercase(*name); + if (std::any_of(definition_.amplitudes.begin(), definition_.amplitudes.end(), + [&canonical](const auto& a) { + return a.name_canonical == canonical; + })) { + diagnostics_.InputFailure( + "duplicate-amplitude", block.location, block.canonical_name, *name, + "Amplitude names must be unique ignoring ASCII case."); + return; + } + TabularAmplitudeDefinition amplitude{*name, canonical, block.location, {}}; + for (const auto& row : block.data) { + if (row.fields.empty() || row.fields.size() % 2U != 0U) { + diagnostics_.InputFailure( + "invalid-amplitude-data", row.location, block.canonical_name, *name, + "Amplitude rows require complete time/value pairs."); + return; + } + for (std::size_t field = 0U; field < row.fields.size(); field += 2U) { + double time = 0.0; + double value = 0.0; + if (!diagnostics_.ParseDouble(row.fields[field], time, row.location, + block.canonical_name) || + !diagnostics_.ParseDouble(row.fields[field + 1U], value, row.location, + block.canonical_name)) + return; + if (time < 0.0 || + (!amplitude.points.empty() && !(time > amplitude.points.back()[0]))) { + diagnostics_.InputFailure( + "invalid-amplitude-data", row.location, block.canonical_name, *name, + "Amplitude times must be nonnegative and strictly increasing."); + return; + } + amplitude.points.push_back({time, value}); + } + } + if (amplitude.points.empty()) { + diagnostics_.InputFailure( + "invalid-amplitude-data", block.location, block.canonical_name, *name, + "Amplitude requires at least one finite time/value pair."); + return; + } + definition_.amplitudes.push_back(std::move(amplitude)); +} + +void StepDefinitionCandidates::ParseInitialConditions( + const KeywordBlock& block) { + if (model_element_family_ != ElementFamily::kThermalShell || + !diagnostics_.ValidateParameters(block, {"TYPE"}) || block.data.empty()) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "invalid-data-arity", block.location, block.canonical_name, "", + "Thermal initial conditions require data rows."); + } + return; + } + const auto* type = diagnostics_.RequiredParameterValue(block, "TYPE"); + if (type == nullptr || !AsciiCaseInsensitiveEquals(*type, "TEMPERATURE")) { + if (!diagnostics_.HasFailure()) + diagnostics_.InputFailure("unsupported-keyword", block.location, + block.canonical_name, "TYPE", + "Only TYPE=TEMPERATURE is supported."); + return; + } + for (const auto& row : block.data) { + if (row.fields.size() != 2U || row.fields[0].empty()) { + diagnostics_.InputFailure("invalid-data-arity", row.location, + block.canonical_name, "", + "Initial temperatures require target, value."); + return; + } + double value = 0.0; + if (!diagnostics_.ParseThermalDouble(row.fields[1], value, row.location, + block.canonical_name)) + return; + thermal_initial_conditions_.push_back({row.fields[0], value, row.location}); + } +} + +void StepDefinitionCandidates::ParseThermalBoundary( + const KeywordBlock& block, std::vector& destination) { + if (!diagnostics_.ValidateParameters(block, {}) || block.data.empty()) { + if (!diagnostics_.HasFailure()) + diagnostics_.InputFailure("invalid-data-arity", block.location, + block.canonical_name, "", + "Thermal BOUNDARY requires data rows."); + return; + } + for (const auto& row : block.data) { + if ((row.fields.size() != 3U && row.fields.size() != 4U) || + row.fields[0].empty()) { + diagnostics_.InputFailure( + "invalid-data-arity", row.location, block.canonical_name, "", + "Thermal boundaries require target, DOF, DOF, value."); + return; + } + std::int64_t first = 0; + std::int64_t last = 0; + if (!diagnostics_.ParseInteger(row.fields[1], first, row.location, + block.canonical_name, true) || + !diagnostics_.ParseInteger(row.fields[2], last, row.location, + block.canonical_name, true) || + first != last || (first != 11 && first != 15)) { + diagnostics_.InputFailure("invalid-dof", row.location, + block.canonical_name, row.fields[0], + "Thermal boundaries support DOF 11 or 15."); + return; + } + double value = 0.0; + if (row.fields.size() == 4U && + !diagnostics_.ParseThermalDouble(row.fields[3], value, row.location, + block.canonical_name)) + return; + destination.push_back( + {row.fields[0], static_cast(first), value, row.location}); + } +} + +void StepDefinitionCandidates::ParseBoundary( + const KeywordBlock& block, + std::vector& destination) { + const bool dynamic_input = std::any_of( + input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) { + return candidate.canonical_name == "DYNAMIC"; + }); + if (!diagnostics_.ValidateParameters(block, {}, + dynamic_input + ? "unsupported-dynamic-boundary" + : "invalid-keyword-parameter") || + block.data.empty()) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure("invalid-data-arity", block.location, + block.canonical_name, "", + "BOUNDARY requires one or more data rows."); + } + return; + } + for (const auto& row : block.data) { + if (row.fields.size() != 3U && row.fields.size() != 4U) { + diagnostics_.InputFailure( + "invalid-data-arity", row.location, block.canonical_name, "", + "BOUNDARY rows require target, first DOF, last DOF, and " + "optional value."); + return; + } + if (row.fields[0].empty()) { + diagnostics_.InputFailure("unresolved-reference", row.location, + block.canonical_name, "", + "A boundary target cannot be empty."); + return; + } + std::int64_t first = 0; + std::int64_t last = 0; + if (!diagnostics_.ParseInteger(row.fields[1], first, row.location, + block.canonical_name, true) || + !diagnostics_.ParseInteger(row.fields[2], last, row.location, + block.canonical_name, true)) { + return; + } + if (first < 1 || first > 6 || last < first || last > 6) { + diagnostics_.InputFailure( + "invalid-dof", row.location, block.canonical_name, row.fields[0], + "Boundary DOFs must be an ordered range in 1..6."); + return; + } + double value = 0.0; + if (row.fields.size() == 4U && + !(model_element_family_ == ElementFamily::kSolid && + row.fields[3].empty()) && + !diagnostics_.ParseDouble(row.fields[3], value, row.location, + block.canonical_name)) { + return; + } + destination.push_back({row.fields[0], static_cast(first), + static_cast(last), value, row.location}); + } +} + +void StepDefinitionCandidates::ParseCload(const KeywordBlock& block, + std::vector& loads) { + if (step_.dynamic && model_element_family_ != ElementFamily::kSolid && + !block.parameters.empty()) { + diagnostics_.InputFailure( + "unsupported-dynamic-load", block.location, block.canonical_name, "", + "Dynamic CLOAD does not support keyword parameters."); + return; + } + const bool solid_dynamic = + step_.dynamic && model_element_family_ == ElementFamily::kSolid; + if (!diagnostics_.ValidateParameters( + block, solid_dynamic ? std::vector{"AMPLITUDE"} + : std::vector{}) || + block.data.empty()) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure("invalid-data-arity", block.location, + block.canonical_name, "", + "CLOAD requires one or more rows."); + } + return; + } + std::optional amplitude_index; + std::string amplitude_name; + if (diagnostics_.Parameter(block, "AMPLITUDE")) { + const auto* name = diagnostics_.RequiredParameterValue(block, "AMPLITUDE"); + if (!name) return; + amplitude_name = *name; + for (std::size_t index = 0U; index < definition_.amplitudes.size(); + ++index) { + if (AsciiCaseInsensitiveEquals(definition_.amplitudes[index].name_raw, + *name)) { + amplitude_index = static_cast(index); + break; + } + } + if (!amplitude_index) { + diagnostics_.InputFailure( + "unresolved-amplitude", block.location, block.canonical_name, *name, + "CLOAD amplitude must resolve to a model-level curve."); + return; + } + } + for (const auto& row : block.data) { + if (row.fields.size() != 3U || row.fields[0].empty()) { + diagnostics_.InputFailure("invalid-data-arity", row.location, + block.canonical_name, "", + "CLOAD rows require target, DOF, magnitude."); + return; + } + std::int64_t dof = 0; + double magnitude = 0.0; + if (!diagnostics_.ParseInteger(row.fields[1], dof, row.location, + block.canonical_name, true) || + !diagnostics_.ParseDouble(row.fields[2], magnitude, row.location, + block.canonical_name)) { + return; + } + if (dof < 1 || dof > 6) { + diagnostics_.InputFailure("invalid-dof", row.location, + block.canonical_name, row.fields[0], + "CLOAD DOF must be in 1..6."); + return; + } + if ((step_.dynamic || model_element_family_ == ElementFamily::kSolid) && + dof > 3) { + diagnostics_.InputFailure( + "unsupported-dynamic-load", row.location, block.canonical_name, + row.fields[1], + "Dynamic CLOAD supports global translational DOF 1 " + "through 3 only."); + return; + } + loads.push_back({row.fields[0], static_cast(dof), magnitude, + row.location, amplitude_index, amplitude_name, + block.location}); + } +} + +void StepDefinitionCandidates::ParseDsload(const KeywordBlock& block) { + if (!diagnostics_.ValidateParameters(block, {}, + "unsupported-dsload-parameter") || + block.data.empty()) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure("invalid-data-arity", block.location, + block.canonical_name, "", + "DSLOAD requires one or more rows."); + } + return; + } + for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) { + const auto& row = block.data[ordinal]; + if (row.fields.size() != 3U || row.fields[0].empty() || + row.fields[1].empty() || row.fields[2].empty()) { + diagnostics_.InputFailure("invalid-data-arity", row.location, + block.canonical_name, "", + "DSLOAD rows require surface, P, magnitude."); + return; + } + if (!IsPressureNameToken(row.fields[0])) { + diagnostics_.InputFailure( + "invalid-surface-name", row.location, block.canonical_name, + row.fields[0], + "DSLOAD surface names must use the approved ASCII name " + "syntax."); + return; + } + if (!AsciiCaseInsensitiveEquals(row.fields[1], "P")) { + diagnostics_.InputFailure( + "unsupported-load-type", row.location, block.canonical_name, + row.fields[1], + "Only uniform surface pressure load type P is supported."); + return; + } + double magnitude = 0.0; + if (!ParseSurfacePressureDouble(row.fields[2], magnitude, row.location, + block.canonical_name)) { + return; + } + step_.surface_pressure_loads.push_back( + {row.fields[0], magnitude, row.fields[2], block.location, row.location, + ordinal, step_.next_load_ordinal++}); + } +} + +void StepDefinitionCandidates::ParseStepStart(const KeywordBlock& block, + bool model_ready) { + if (step_seen_) { + if (step_.frequency || step_.dynamic) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, + block.canonical_name, "", + "The frequency subset accepts exactly one procedure step."); + } else { + const auto* nlgeom = diagnostics_.Parameter(block, "NLGEOM"); + const bool nonlinear_second_step = + nlgeom != nullptr && nlgeom->value && + AsciiCaseInsensitiveEquals(*nlgeom->value, "YES"); + if (step_.nlgeom || nonlinear_second_step) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, + block.canonical_name, "", + "The nonlinear static subset accepts exactly one " + "procedure step."); + } else { + diagnostics_.InputFailure("unsupported-multiple-step", block.location, + block.canonical_name, "", + "V0 accepts exactly one analysis step."); + } + } + return; + } + if (!model_ready) { + diagnostics_.InvalidKeywordLocation( + block, + "The sole step follows the complete assembly and " + "material definitions."); + return; + } + if (!diagnostics_.ValidateParameters( + block, {"NAME", "NLGEOM", "PERTURBATION", "INC"}) || + !diagnostics_.RequireNoData(block)) { + return; + } + StaticProcedureKind procedure_kind = + StaticProcedureKind::kLinearSmallDisplacement; + bool nlgeom = false; + const auto* nlgeom_parameter = diagnostics_.Parameter(block, "NLGEOM"); + if (nlgeom_parameter != nullptr) { + if (!nlgeom_parameter->value || nlgeom_parameter->value->empty()) { + diagnostics_.InputFailure( + "invalid-keyword-parameter", block.location, block.canonical_name, + "NLGEOM", "NLGEOM requires YES or NO in the approved subset."); + return; + } + if (AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "YES")) { + if (model_element_family_ != ElementFamily::kShell && + model_element_family_ != ElementFamily::kSolid) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, + block.canonical_name, *nlgeom_parameter->value, + "NLGEOM=YES requires a supported Shell or Solid model."); + return; + } + procedure_kind = StaticProcedureKind::kNonlinearFullNewton; + nlgeom = true; + } else if (!AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "NO")) { + diagnostics_.InputFailure("unsupported-analysis-procedure", + block.location, block.canonical_name, + *nlgeom_parameter->value, + "NLGEOM must select YES or NO."); + return; + } + } + const auto* perturbation = diagnostics_.Parameter(block, "PERTURBATION"); + if (perturbation != nullptr && perturbation->value) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, block.canonical_name, + "PERTURBATION", "PERTURBATION is accepted only as a value-less flag."); + return; + } + const auto* name = diagnostics_.Parameter(block, "NAME"); + if (name != nullptr && (!name->value || name->value->empty())) { + diagnostics_.InputFailure( + "invalid-keyword-parameter", block.location, block.canonical_name, + "NAME", "STEP NAME must not be empty when it is present."); + return; + } + const auto* increment_limit = diagnostics_.Parameter(block, "INC"); + std::uint32_t maximum_increments = 100U; + if (increment_limit != nullptr) { + if (!increment_limit->value || increment_limit->value->empty()) { + diagnostics_.InputFailure("invalid-dynamic-data", block.location, + block.canonical_name, "INC", + "INC requires a positive uint32 value."); + return; + } + std::int64_t parsed_limit = 0; + if (!diagnostics_.ParseInteger(*increment_limit->value, parsed_limit, + block.location, block.canonical_name, + true) || + parsed_limit > static_cast( + std::numeric_limits::max())) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure("invalid-dynamic-data", block.location, + block.canonical_name, "INC", + "INC requires a positive uint32 value."); + } + return; + } + maximum_increments = static_cast(parsed_limit); + } + step_seen_ = true; + in_step_ = true; + step_ = {}; + step_.location = block.location; + step_.name_raw = name == nullptr ? "Step-1" : *name->value; + step_.name_canonical = AsciiUppercase(step_.name_raw); + step_.procedure_kind = procedure_kind; + step_.nlgeom = nlgeom; + step_.nlgeom_explicit = nlgeom_parameter != nullptr; + step_.perturbation_explicit = perturbation != nullptr; + step_.maximum_increments = maximum_increments; + step_load_seen_ = false; + step_no_op_seen_ = false; +} + +void StepDefinitionCandidates::ParseStepBlock(const KeywordBlock& block) { + if (model_element_family_ == ElementFamily::kSolid && + (block.canonical_name == "STATIC" || + block.canonical_name == "FREQUENCY" || + block.canonical_name == "DYNAMIC") && + (step_.has_static || step_.frequency || step_.dynamic)) { + diagnostics_.InputFailure("unsupported-analysis-procedure", block.location, + block.canonical_name, step_.name_raw, + "Solid steps accept exactly one procedure."); + return; + } + if (model_element_family_ == ElementFamily::kThermalShell && + (block.canonical_name == "STATIC" || + block.canonical_name == "FREQUENCY" || + block.canonical_name == "DYNAMIC")) { + diagnostics_.InputFailure("unsupported-analysis-procedure", block.location, + block.canonical_name, step_.name_raw, + "DS4 supports only one HEAT TRANSFER procedure."); + return; + } + if (block.canonical_name == "END STEP") { + active_output_ = false; + if (!diagnostics_.ValidateParameters(block, {}) || + !diagnostics_.RequireNoData(block)) { + return; + } + if (!step_.has_static && !step_.frequency && !step_.dynamic && + !step_.has_heat_transfer) { + diagnostics_.InvalidKeywordLocation( + block, "The sole step requires one supported procedure keyword."); + return; + } + in_step_ = false; + return; + } + if (block.canonical_name == "STEP") { + diagnostics_.InputFailure("unsupported-multiple-step", block.location, + block.canonical_name, "", + "A second or nested step is unsupported."); + return; + } + if (block.canonical_name == "HEAT TRANSFER") { + active_output_ = false; + if (step_.has_static || step_.frequency || step_.dynamic || + step_.has_heat_transfer || !step_.boundaries.empty() || + !step_.loads.empty() || step_no_op_seen_) { + diagnostics_.InvalidKeywordLocation( + block, "HEAT TRANSFER must be the first procedure."); + return; + } + ParseHeatTransfer(block); + } else if (block.canonical_name == "STATIC") { + active_output_ = false; + if (!step_.boundaries.empty() || !step_.loads.empty() || + !step_.surface_pressure_loads.empty() || step_no_op_seen_) { + diagnostics_.InvalidKeywordLocation( + block, "STATIC must be the first keyword in the sole step."); + return; + } + ParseStatic(block); + } else if (block.canonical_name == "FREQUENCY") { + active_output_ = false; + if (step_.has_static || step_.frequency || !step_.boundaries.empty() || + !step_.loads.empty() || !step_.surface_pressure_loads.empty() || + step_no_op_seen_) { + diagnostics_.InvalidKeywordLocation( + block, "FREQUENCY must be the first procedure in the sole step."); + return; + } + ParseFrequency(block); + } else if (block.canonical_name == "DYNAMIC") { + active_output_ = false; + if (step_.has_static || step_.frequency || step_.dynamic || + !step_.boundaries.empty() || !step_.loads.empty() || + !step_.surface_pressure_loads.empty() || step_no_op_seen_) { + diagnostics_.InvalidKeywordLocation( + block, "DYNAMIC must be the first procedure in the sole step."); + return; + } + ParseDynamic(block); + } else if (block.canonical_name == "BOUNDARY") { + active_output_ = false; + if ((!step_.has_static && !step_.frequency && !step_.dynamic && + !step_.has_heat_transfer) || + step_load_seen_ || step_no_op_seen_) { + diagnostics_.InvalidKeywordLocation( + block, + "Step boundaries follow STATIC and precede loads " + "and no-op requests."); + return; + } + if (step_.dynamic && !block.parameters.empty()) { + diagnostics_.InputFailure( + "unsupported-dynamic-boundary", block.location, block.canonical_name, + "", "Dynamic BOUNDARY does not support keyword parameters."); + return; + } + if (step_.has_heat_transfer) { + ParseThermalBoundary(block, step_.thermal_boundaries); + } else { + ParseBoundary(block, step_.boundaries); + } + if (!diagnostics_.HasFailure() && step_.frequency) { + ValidateEigenBoundaries(step_.boundaries); + } + if (!diagnostics_.HasFailure() && step_.dynamic) { + ValidateDynamicBoundaries(step_.boundaries); + } + } else if (block.canonical_name == "CLOAD") { + active_output_ = false; + if ((!step_.has_static && !step_.dynamic) || step_no_op_seen_) { + diagnostics_.InvalidKeywordLocation( + block, + "CLOAD follows STATIC and boundaries and precedes no-op requests."); + return; + } + ParseCload(block, step_.loads); + if (!diagnostics_.HasFailure()) { + step_.next_load_ordinal += block.data.size(); + } + step_load_seen_ = !diagnostics_.HasFailure(); + } else if (block.canonical_name == "CFLUX") { + active_output_ = false; + if (!step_.has_heat_transfer || step_no_op_seen_) { + diagnostics_.InvalidKeywordLocation( + block, "CFLUX follows HEAT TRANSFER and boundaries."); + return; + } + ParseCflux(block); + step_load_seen_ = !diagnostics_.HasFailure(); + } else if (block.canonical_name == "DSLOAD") { + active_output_ = false; + if (!step_.has_static || step_.dynamic || step_no_op_seen_) { + diagnostics_.InvalidKeywordLocation( + block, + "DSLOAD follows STATIC and boundaries and " + "precedes no-op requests."); + return; + } + ParseDsload(block); + step_load_seen_ = !diagnostics_.HasFailure(); + } else if (block.canonical_name == "RESTART") { + active_output_ = false; + if (!step_.has_static && !step_.frequency && !step_.dynamic && + !step_.has_heat_transfer) { + diagnostics_.InvalidKeywordLocation( + block, "Step no-op requests follow STATIC, boundaries, and loads."); + return; + } + step_no_op_seen_ = true; + if (step_.frequency || step_.has_heat_transfer || + model_element_family_ == ElementFamily::kSolid) { + const auto* write = diagnostics_.Parameter(block, "WRITE"); + const auto* frequency = diagnostics_.Parameter(block, "FREQUENCY"); + if (!diagnostics_.ValidateParameters(block, {"WRITE", "FREQUENCY"}) || + write == nullptr || write->value || frequency == nullptr || + !frequency->value || *frequency->value != "0") { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Supported RESTART no-op requires WRITE, FREQUENCY=0."); + } + return; + } + } + if (diagnostics_.RequireNoData(block)) { + AddIgnoredWarning(block); + } + } else if (block.canonical_name == "OUTPUT") { + if (!step_.has_static && !step_.frequency && !step_.dynamic && + !step_.has_heat_transfer) { + diagnostics_.InvalidKeywordLocation( + block, "Step no-op requests follow STATIC, boundaries, and loads."); + return; + } + step_no_op_seen_ = true; + ParseOutputRoot(block); + } else if (block.canonical_name == "NODE OUTPUT" || + block.canonical_name == "ELEMENT OUTPUT" || + block.canonical_name == "CONTACT OUTPUT" || + block.canonical_name == "ENERGY OUTPUT") { + ParseOutputChild(block); + } else { + active_output_ = false; + if (step_.procedure_kind == StaticProcedureKind::kNonlinearFullNewton && + !step_.has_static) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, + block.canonical_name, "", + "The nonlinear step supports only the STATIC procedure."); + return; + } + diagnostics_.RejectUnknown(block, + model_element_family_ == ElementFamily::kShell); + } +} + +bool StepDefinitionCandidates::ParseFrequencyModeCount( + const std::string& text, std::int64_t& value, + const SourceLocation& location) { + if (text.empty() || + !std::all_of(text.begin(), text.end(), [](char character) { + return character >= '0' && character <= '9'; + })) { + return diagnostics_.InputFailure( + "invalid-frequency-data", location, "FREQUENCY", text, + "The requested mode count must be positive decimal digits."); + } + const auto parsed = + std::from_chars(text.data(), text.data() + text.size(), value); + if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size() || + value <= 0) { + return diagnostics_.InputFailure( + "invalid-frequency-data", location, "FREQUENCY", text, + "The requested mode count must be a positive int64 value."); + } + return true; +} + +void StepDefinitionCandidates::ParseFrequency(const KeywordBlock& block) { + if (step_.nlgeom || (step_.perturbation_explicit && step_.has_static) || + !diagnostics_.ValidateParameters( + block, {"EIGENSOLVER", "SIM", "ACOUSTIC COUPLING", "NORMALIZATION"}, + "unsupported-frequency-option")) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, + block.canonical_name, "", + "FREQUENCY requires the original linear configuration."); + } + return; + } + if (block.data.size() != 1U || block.data[0].fields.empty() || + block.data[0].fields.size() > 7U) { + diagnostics_.InputFailure( + "invalid-frequency-data", block.location, block.canonical_name, "", + "FREQUENCY requires one data row with one to seven fields."); + return; + } + for (std::size_t field = 1U; field < block.data[0].fields.size(); ++field) { + if (!block.data[0].fields[field].empty()) { + diagnostics_.InputFailure( + "invalid-frequency-data", block.data[0].location, + block.canonical_name, std::to_string(field + 1U), + "Only the requested mode-count field may be non-empty."); + return; + } + } + FrequencyStepDefinition frequency{step_.name_raw, + step_.name_canonical, + step_.location, + block.location, + block.data[0].location, + 0, + step_.nlgeom_explicit, + step_.perturbation_explicit, + false, + false, + false, + false, + FrequencyEigensolver::kLanczos, + FrequencyNormalization::kMass, + {}}; + if (!ParseFrequencyModeCount(block.data[0].fields[0], + frequency.requested_mode_count, + block.data[0].location)) { + return; + } + const auto* eigensolver = diagnostics_.Parameter(block, "EIGENSOLVER"); + if (eigensolver != nullptr) { + if (!eigensolver->value || + !AsciiCaseInsensitiveEquals(*eigensolver->value, "LANCZOS")) { + diagnostics_.InputFailure("unsupported-frequency-option", block.location, + block.canonical_name, "EIGENSOLVER", + "Only EIGENSOLVER=LANCZOS is accepted."); + return; + } + frequency.eigensolver_explicit = true; + } + const auto* sim = diagnostics_.Parameter(block, "SIM"); + if (sim != nullptr) { + if (sim->value && !AsciiCaseInsensitiveEquals(*sim->value, "YES")) { + diagnostics_.InputFailure("unsupported-frequency-option", block.location, + block.canonical_name, "SIM", + "SIM accepts only a flag or YES."); + return; + } + frequency.sim_explicit = true; + } + const auto* acoustic = diagnostics_.Parameter(block, "ACOUSTIC COUPLING"); + if (acoustic != nullptr) { + if (!acoustic->value || + !AsciiCaseInsensitiveEquals(*acoustic->value, "ON")) { + diagnostics_.InputFailure("unsupported-frequency-option", block.location, + block.canonical_name, "ACOUSTIC COUPLING", + "Only ACOUSTIC COUPLING=ON is accepted."); + return; + } + frequency.acoustic_coupling_explicit = true; + } + const auto* normalization = diagnostics_.Parameter(block, "NORMALIZATION"); + if (normalization != nullptr) { + if (!normalization->value || + !AsciiCaseInsensitiveEquals(*normalization->value, "MASS")) { + diagnostics_.InputFailure("unsupported-frequency-option", block.location, + block.canonical_name, "NORMALIZATION", + "Only NORMALIZATION=MASS is accepted."); + return; + } + frequency.normalization_explicit = true; + } + step_.frequency = std::move(frequency); + definition_.warnings.push_back( + {Severity::kWarning, "source-eigensolver-mapped-to-feast", block.location, + block.canonical_name, step_.name_raw, + "The accepted source eigensolver is mapped to the FEAST backend."}); + if (step_.frequency->sim_explicit) { + definition_.warnings.push_back( + {Severity::kWarning, "source-sim-option-not-applied", block.location, + block.canonical_name, step_.name_raw, + "The accepted source SIM option has no FESA runtime effect."}); + } + if (step_.frequency->acoustic_coupling_explicit) { + definition_.warnings.push_back( + {Severity::kWarning, "acoustic-option-inapplicable-structural-only", + block.location, block.canonical_name, step_.name_raw, + "Acoustic coupling is inapplicable to this structural-only model."}); + } +} + +void StepDefinitionCandidates::ValidateEigenBoundaries( + const std::vector& boundaries) { + for (const auto& boundary : boundaries) { + if (boundary.value != 0.0) { + diagnostics_.InputFailure( + "unsupported-eigen-boundary", boundary.location, "BOUNDARY", + boundary.target, "Frequency boundaries must prescribe exact zero."); + return; + } + } +} + +void StepDefinitionCandidates::ValidateDynamicBoundaries( + const std::vector& boundaries) { + for (const auto& boundary : boundaries) { + if (boundary.value != 0.0) { + diagnostics_.InputFailure( + "unsupported-dynamic-boundary", boundary.location, "BOUNDARY", + boundary.target, + "Dynamic boundaries must be homogeneous and constant."); + return; + } + } +} + +void StepDefinitionCandidates::ParseDynamic(const KeywordBlock& block) { + if (step_.perturbation_explicit || + (model_element_family_ != ElementFamily::kSolid && + (!step_.nlgeom || model_element_family_ != ElementFamily::kShell)) || + !diagnostics_.ValidateParameters(block, {"DIRECT"}, + "unsupported-dynamic-parameter")) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, + block.canonical_name, "", + "DYNAMIC requires nonlinear Shell or linear/nonlinear " + "Solid geometry."); + } + return; + } + if (model_element_family_ == ElementFamily::kSolid && + block.data.size() == 1U && block.data[0].fields.size() > 3U) { + diagnostics_.InputFailure( + "invalid-dynamic-data", block.data[0].location, block.canonical_name, + "", "Solid DYNAMIC requires h,T with at most one blank field."); + return; + } + const auto* direct = diagnostics_.Parameter(block, "DIRECT"); + if (direct == nullptr || direct->value || block.data.size() != 1U || + block.data[0].fields.size() < 2U || + std::any_of(block.data[0].fields.begin() + 2, block.data[0].fields.end(), + [](const auto& field) { return !field.empty(); })) { + diagnostics_.InputFailure( + "invalid-dynamic-data", block.location, block.canonical_name, "", + "DYNAMIC,DIRECT requires one row with increment and time period."); + return; + } + DynamicStepDefinition dynamic{step_.name_raw, + step_.name_canonical, + step_.location, + block.location, + block.data[0].location, + 0.0, + 0.0, + 0U, + step_.maximum_increments, + -0.05, + 0.275625, + 0.55, + {}, + {}}; + if (!ParseDynamicDouble(block.data[0].fields[0], dynamic.initial_increment, + block.data[0].location, block.canonical_name) || + !ParseDynamicDouble(block.data[0].fields[1], dynamic.time_period, + block.data[0].location, block.canonical_name) || + !(dynamic.initial_increment > 0.0) || !(dynamic.time_period > 0.0)) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "invalid-dynamic-data", block.data[0].location, block.canonical_name, + "", "Dynamic increment and time period must be finite and positive."); + } + return; + } + const double count = dynamic.time_period / dynamic.initial_increment; + const double rounded_count = std::round(count); + if (!std::isfinite(count) || rounded_count < 1.0 || + rounded_count > + static_cast(std::numeric_limits::max()) || + dynamic.initial_increment * rounded_count != dynamic.time_period) { + diagnostics_.InputFailure( + "invalid-dynamic-schedule", block.data[0].location, + block.canonical_name, "", + "DYNAMIC,DIRECT requires an exact positive integer number " + "of increments."); + return; + } + dynamic.increment_count = static_cast(rounded_count); + if (dynamic.increment_count > dynamic.maximum_increments) { + diagnostics_.InputFailure( + "invalid-dynamic-schedule", block.data[0].location, + block.canonical_name, "", + "DYNAMIC,DIRECT increment count cannot exceed STEP INC."); + return; + } + dynamic.nlgeom = step_.nlgeom; + step_.dynamic = std::move(dynamic); +} + +void StepDefinitionCandidates::ParseHeatTransfer(const KeywordBlock& block) { + if (model_element_family_ != ElementFamily::kThermalShell || step_.nlgeom || + step_.perturbation_explicit || + !diagnostics_.ValidateParameters(block, {"END"}, + "unsupported-thermal-option") || + block.data.size() != 1U) { + if (!diagnostics_.HasFailure()) + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, + block.canonical_name, "", + "HEAT TRANSFER requires the DS4 thermal subset."); + return; + } + const auto* end = diagnostics_.Parameter(block, "END"); + if (end != nullptr && + (!end->value || !AsciiCaseInsensitiveEquals(*end->value, "PERIOD"))) { + diagnostics_.InputFailure("unsupported-thermal-option", block.location, + block.canonical_name, "END", + "HEAT TRANSFER accepts only END=PERIOD."); + return; + } + if (block.data[0].fields.size() > 5U) { + diagnostics_.InputFailure( + "invalid-thermal-time-grid", block.data[0].location, + block.canonical_name, "", + "HEAT TRANSFER permits at most three trailing empty fields."); + return; + } + const auto fields = WithoutTrailingEmpty(block.data[0].fields); + if (fields.size() != 2U) { + diagnostics_.InputFailure( + "invalid-thermal-time-grid", block.data[0].location, + block.canonical_name, "", + "HEAT TRANSFER requires increment and time period."); + return; + } + if (!diagnostics_.ParseThermalDouble(fields[0], step_.thermal_increment, + block.data[0].location, + block.canonical_name) || + !diagnostics_.ParseThermalDouble(fields[1], step_.thermal_period, + block.data[0].location, + block.canonical_name) || + !(step_.thermal_increment > 0.0) || !(step_.thermal_period > 0.0)) { + if (!diagnostics_.HasFailure()) + diagnostics_.InputFailure( + "invalid-thermal-time-grid", block.data[0].location, + block.canonical_name, "", + "Thermal increment and period must be finite and positive."); + return; + } + const auto grid = ThermalTimeGrid::Create( + step_.thermal_period, step_.thermal_increment, step_.maximum_increments); + if (!grid.HasValue()) { + auto diagnostic = grid.GetStatus().Diagnostics().front(); + diagnostic.location = block.data[0].location; + diagnostic.keyword = block.canonical_name; + diagnostic.entity_identity = step_.name_raw; + diagnostics_.Fail( + grid.GetStatus().Category().value_or(FailureCategory::kInput), + std::move(diagnostic.code), diagnostic.location, + std::move(diagnostic.keyword), std::move(diagnostic.entity_identity), + std::move(diagnostic.message)); + return; + } + step_.has_heat_transfer = true; +} + +void StepDefinitionCandidates::ParseCflux(const KeywordBlock& block) { + if (!diagnostics_.ValidateParameters(block, {}) || block.data.empty()) { + if (!diagnostics_.HasFailure()) + diagnostics_.InputFailure("invalid-data-arity", block.location, + block.canonical_name, "", + "CFLUX requires target, DOF, power rows."); + return; + } + for (const auto& row : block.data) { + if (row.fields.size() != 3U || row.fields[0].empty()) { + diagnostics_.InputFailure("invalid-data-arity", row.location, + block.canonical_name, "", + "CFLUX rows require target, DOF, power."); + return; + } + std::int64_t dof = 0; + double power = 0.0; + if (!diagnostics_.ParseInteger(row.fields[1], dof, row.location, + block.canonical_name, true) || + !diagnostics_.ParseThermalDouble(row.fields[2], power, row.location, + block.canonical_name) || + (dof != 11 && dof != 15)) { + diagnostics_.InputFailure("invalid-dof", row.location, + block.canonical_name, row.fields[0], + "CFLUX supports DOF 11 or 15."); + return; + } + step_.thermal_fluxes.push_back( + {row.fields[0], static_cast(dof), power, row.location}); + } +} + +void StepDefinitionCandidates::ParseStatic(const KeywordBlock& block) { + if (model_element_family_ == ElementFamily::kSolid && + diagnostics_.Parameter(block, "DIRECT")) { + if (step_.perturbation_explicit || step_.has_static || step_.frequency || + step_.dynamic) { + diagnostics_.InputFailure( + "unsupported-analysis-procedure", block.location, + block.canonical_name, step_.name_raw, + "STATIC DIRECT requires one non-perturbation procedure."); + return; + } + if (!diagnostics_.ValidateParameters(block, {"DIRECT"})) return; + const auto* direct = diagnostics_.Parameter(block, "DIRECT"); + if (direct->value || block.data.size() != 1U || + (block.data[0].fields.size() < 2U || block.data[0].fields.size() > 3U || + std::any_of(block.data[0].fields.begin() + 2, + block.data[0].fields.end(), + [](const auto& field) { return !field.empty(); }))) { + diagnostics_.InputFailure("invalid-static-data", block.location, + block.canonical_name, "DIRECT", + "STATIC DIRECT requires exactly h,T."); + return; + } + double h = 0.0; + double period = 0.0; + if (!diagnostics_.ParseDouble(block.data[0].fields[0], h, + block.data[0].location, "STATIC") || + !diagnostics_.ParseDouble(block.data[0].fields[1], period, + block.data[0].location, "STATIC")) + return; + const double count = std::round(period / h); + if (!(h > 0.0) || !(period > 0.0) || !std::isfinite(count) || count < 1.0 || + count > static_cast(step_.maximum_increments) || + h * count != period) { + diagnostics_.InputFailure( + "invalid-static-schedule", block.data[0].location, + block.canonical_name, "DIRECT", + "STATIC DIRECT requires positive exact N*h=T and N<=INC."); + return; + } + step_.static_values = {h, period, h, h}; + step_.static_direct = true; + step_.static_increment_count = static_cast(count); + step_.has_static = true; + return; + } + if (step_.perturbation_explicit || step_.frequency) { + diagnostics_.InputFailure("unsupported-analysis-procedure", block.location, + block.canonical_name, "PERTURBATION", + "PERTURBATION is supported only with FREQUENCY."); + return; + } + if (step_.has_heat_transfer || step_.has_static || + !diagnostics_.ValidateParameters(block, {}) || block.data.size() != 1U || + block.data[0].fields.size() != 4U) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + step_.nlgeom ? "invalid-nonlinear-static-data" + : "invalid-static-data", + block.location, block.canonical_name, "", + "STATIC requires exactly one row of four values."); + } + return; + } + for (std::size_t field = 0U; field < 4U; ++field) { + const std::string& text = block.data[0].fields[field]; + const bool parsed = step_.nlgeom + ? ParseNonlinearStaticDouble( + text, step_.static_values[field], + block.data[0].location, block.canonical_name) + : diagnostics_.ParseDouble( + text, step_.static_values[field], + block.data[0].location, block.canonical_name); + if (!parsed) { + return; + } + if (step_.static_values[field] <= 0.0) { + diagnostics_.InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data" + : "invalid-static-data", + block.data[0].location, block.canonical_name, + "", "All four STATIC fields must be positive."); + return; + } + } + if (step_.static_values[2] > step_.static_values[3]) { + diagnostics_.InputFailure( + step_.nlgeom ? "invalid-nonlinear-static-data" : "invalid-static-data", + block.data[0].location, block.canonical_name, "", + "STATIC minimum increment cannot exceed maximum increment."); + return; + } + if (step_.nlgeom && step_.static_values[1] != 1.0) { + diagnostics_.InputFailure( + "invalid-nonlinear-static-data", block.data[0].location, + block.canonical_name, "", + "The nonlinear STATIC time period must be exactly 1.0."); + return; + } + step_.has_static = true; +} + +void StepDefinitionCandidates::ParseOutputRoot(const KeywordBlock& block) { + if (step_.has_heat_transfer) { + const auto* field = diagnostics_.Parameter(block, "FIELD"); + const auto* history = diagnostics_.Parameter(block, "HISTORY"); + const auto* variable = diagnostics_.Parameter(block, "VARIABLE"); + const auto* frequency = diagnostics_.Parameter(block, "FREQUENCY"); + const bool valid_field = + field && !field->value && variable && variable->value && + AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"); + const bool valid_history = history && !history->value && frequency && + frequency->value && *frequency->value == "0"; + if (!diagnostics_.RequireNoData(block)) return; + if (block.parameters.size() != 2U || (!valid_field && !valid_history)) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Thermal OUTPUT accepts FIELD, VARIABLE=PRESELECT or " + "HISTORY, FREQUENCY=0."); + return; + } + AddIgnoredWarning(block); + return; + } + if (!diagnostics_.ValidateParameters( + block, (step_.dynamic || step_.has_heat_transfer) + ? std::vector{"FIELD", "HISTORY", + "VARIABLE", "FREQUENCY"} + : std::vector{"FIELD", "HISTORY", + "VARIABLE"})) { + return; + } + const auto* field = diagnostics_.Parameter(block, "FIELD"); + const auto* history = diagnostics_.Parameter(block, "HISTORY"); + const auto* variable = diagnostics_.Parameter(block, "VARIABLE"); + if ((field == nullptr) == (history == nullptr) || + (field != nullptr && field->value) || + (history != nullptr && history->value)) { + diagnostics_.InputFailure( + step_.nlgeom ? "invalid-keyword-parameter" : "unsupported-keyword", + block.location, block.canonical_name, "", + "OUTPUT must select exactly FIELD or HISTORY."); + return; + } + if (model_element_family_ == ElementFamily::kSolid && + ((variable && (!variable->value || !AsciiCaseInsensitiveEquals( + *variable->value, "PRESELECT"))) || + !diagnostics_.RequireNoData(block))) { + if (!diagnostics_.HasFailure()) + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, + "VARIABLE", "Solid OUTPUT accepts only PRESELECT requests."); + return; + } + if (step_.nlgeom && + (variable == nullptr || !variable->value || + !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) { + diagnostics_.InputFailure("invalid-keyword-parameter", block.location, + block.canonical_name, "VARIABLE", + "Nonlinear OUTPUT requires VARIABLE=PRESELECT."); + return; + } + if (step_.dynamic) { + const auto* frequency = diagnostics_.Parameter(block, "FREQUENCY"); + if (!diagnostics_.ValidateParameters( + block, {"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"}) || + variable == nullptr || !variable->value || + !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT") || + (frequency != nullptr && + (!frequency->value || *frequency->value != "1")) || + !diagnostics_.RequireNoData(block)) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Dynamic OUTPUT accepts only VARIABLE=PRESELECT, FREQUENCY=1."); + } + return; + } + } else if (step_.frequency && + ((variable != nullptr && + (!variable->value || + !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) || + !diagnostics_.RequireNoData(block))) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Frequency OUTPUT accepts only VARIABLE=PRESELECT."); + } + return; + } + active_output_ = true; + active_output_is_field_ = field != nullptr; + AddIgnoredWarning(block); +} + +void StepDefinitionCandidates::ParseOutputChild(const KeywordBlock& block) { + if (step_.has_heat_transfer) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Thermal output children are outside the approved subset."); + return; + } + if (!active_output_) { + diagnostics_.InputFailure( + "invalid-keyword-location", block.location, block.canonical_name, "", + "Output variable keywords require an active OUTPUT request."); + return; + } + if (model_element_family_ == ElementFamily::kSolid && step_.dynamic) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Solid dynamic output children are outside the approved subset."); + return; + } + if (model_element_family_ == ElementFamily::kSolid && step_.has_static) { + const auto* variable = diagnostics_.Parameter(block, "VARIABLE"); + const auto* directions = diagnostics_.Parameter(block, "DIRECTIONS"); + const bool node = block.canonical_name == "NODE OUTPUT"; + const bool element = block.canonical_name == "ELEMENT OUTPUT"; + const bool contact = block.canonical_name == "CONTACT OUTPUT"; + if (!active_output_is_field_ || (!node && !element && !contact) || + !diagnostics_.ValidateParameters( + block, element + ? std::vector{"VARIABLE", "DIRECTIONS"} + : std::vector{"VARIABLE"}) || + (directions && + (!directions->value || + !AsciiCaseInsensitiveEquals(*directions->value, "YES"))) || + (variable && + (!variable->value || + !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) || + (contact && (!variable || !block.data.empty()))) { + if (!diagnostics_.HasFailure()) + diagnostics_.InputFailure("unsupported-keyword", block.location, + block.canonical_name, "", + "Solid static output child is unsupported."); + return; + } + for (const auto& row : block.data) { + for (const auto& field : row.fields) { + const std::string token = AsciiUppercase(field); + if (!(node ? (token == "U" || token == "RF") + : (token == "S" || token == "E"))) { + diagnostics_.InputFailure( + "unsupported-keyword", row.location, block.canonical_name, field, + "Solid output quantities must be physical supported " + "components."); + return; + } + } + } + } + if (step_.frequency) { + const std::string_view required_variable = + block.canonical_name == "NODE OUTPUT" ? "PRESELECT" : "ALL"; + const auto* variable = diagnostics_.Parameter(block, "VARIABLE"); + if ((block.canonical_name != "NODE OUTPUT" && + block.canonical_name != "ENERGY OUTPUT") || + (block.canonical_name == "NODE OUTPUT" && !active_output_is_field_) || + (block.canonical_name == "ENERGY OUTPUT" && active_output_is_field_) || + !diagnostics_.ValidateParameters(block, {"VARIABLE"}) || + variable == nullptr || !variable->value || + !AsciiCaseInsensitiveEquals(*variable->value, required_variable) || + !diagnostics_.RequireNoData(block)) { + if (!diagnostics_.HasFailure()) { + diagnostics_.InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Frequency output child is outside the approved subset."); + } + return; + } + } + AddIgnoredWarning(block); +} + +void StepDefinitionCandidates::AddIgnoredWarning(const KeywordBlock& block) { + // One warning per allowlisted keyword keeps no-op provenance stable; + // subordinate variable rows remain attached to that keyword record. + definition_.warnings.push_back( + {Severity::kWarning, + step_.dynamic ? "ignored-output-request" : "ignored-input-keyword", + block.location, block.canonical_name, "", + "The allowlisted Abaqus keyword is " + "ignored without semantic effect."}); +} + +std::optional> +StepDefinitionCandidates::ResolveNodeTarget( + const SourceTargetResolver& resolver, const std::string& target, + const SourceLocation& location, const std::string& keyword) { + auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", target}); + if (!resolved.HasValue()) { + diagnostics_.InputFailure( + "unresolved-reference", location, keyword, target, + "The boundary or load target must resolve unambiguously to one " + "node or one node set."); + return std::nullopt; + } + std::vector indices; + indices.reserve(resolved.Value().size()); + for (const auto& entry : resolved.Value()) { + if (model_element_family_ == ElementFamily::kThermalShell && + !thermal_active_nodes_[entry.entity_index]) { + diagnostics_.ModelFailure( + "inactive-thermal-target", location, keyword, target, + "Thermal targets must connect to an active DS4 element."); + return std::nullopt; + } + indices.push_back(entry.entity_index); + } + return indices; +} + +void StepDefinitionCandidates::FinalizeStep() { + if (step_.has_heat_transfer) { + thermal_active_nodes_.assign(definition_.nodes.size(), false); + for (const auto& element : definition_.thermal_elements) + for (const auto node : element.node_indices) + thermal_active_nodes_[node] = true; + const SourceTargetIndex target_index = SourceTargetIndex::FromNodeDefinitions(definition_.nodes, definition_.node_sets); + const SourceTargetResolver resolver{target_index}; + const auto append_boundaries = + [this, &resolver]( + const std::vector& rows, + std::map, double>* values, + std::vector* out) { + for (const auto& row : rows) { + const auto nodes = ResolveNodeTarget(resolver, row.target, + row.location, "BOUNDARY"); + if (!nodes) return false; + for (const EntityIndex node : *nodes) { + const auto key = std::make_pair(node, row.dof); + const auto existing = values->find(key); + if (existing != values->end() && existing->second != row.value) { + return diagnostics_.InputFailure( + "conflicting-boundary-condition", row.location, "BOUNDARY", + row.target, + "Expanded thermal boundaries prescribe " + "different values."); + } + (*values)[key] = row.value; + out->push_back({node, row.dof, row.value, row.location}); + } + } + return true; + }; + std::map, double> model_values; + if (!append_boundaries(thermal_model_boundaries_, &model_values, + &definition_.thermal_model_boundaries)) + return; + std::vector step_boundaries; + std::map, double> step_values; + if (!append_boundaries(step_.thermal_boundaries, &step_values, + &step_boundaries)) + return; + std::map initial_values; + for (const auto& row : thermal_initial_conditions_) { + const auto nodes = ResolveNodeTarget(resolver, row.target, row.location, + "INITIAL CONDITIONS"); + if (!nodes) return; + for (const EntityIndex node : *nodes) { + const auto inserted = initial_values.emplace(node, row.value); + if (!inserted.second && inserted.first->second != row.value) { + diagnostics_.InputFailure( + "conflicting-initial-temperature", row.location, + "INITIAL CONDITIONS", row.target, + "Expanded initial temperatures prescribe different values."); + return; + } + definition_.thermal_initial_conditions.push_back( + {node, row.value, row.location}); + } + } + std::vector loads; + for (const auto& row : step_.thermal_fluxes) { + const auto nodes = + ResolveNodeTarget(resolver, row.target, row.location, "CFLUX"); + if (!nodes) return; + for (const EntityIndex node : *nodes) { + loads.push_back({node, row.dof, row.power, row.location, loads.size()}); + } + } + definition_.thermal_step = {step_.name_raw, + step_.thermal_increment, + step_.thermal_period, + step_.maximum_increments, + false, + std::move(step_boundaries), + std::move(loads), + step_.location}; + return; + } + std::vector boundaries = model_boundaries_; + boundaries.insert(boundaries.end(), step_.boundaries.begin(), + step_.boundaries.end()); + if (step_.frequency) { + ValidateEigenBoundaries(boundaries); + if (diagnostics_.HasFailure()) { + return; + } + } + if (step_.dynamic) { + ValidateDynamicBoundaries(boundaries); + if (diagnostics_.HasFailure()) { + return; + } + } + const SourceTargetIndex target_index = SourceTargetIndex::FromNodeDefinitions(definition_.nodes, definition_.node_sets); + const SourceTargetResolver target_resolver{target_index}; + + if (model_element_family_ == ElementFamily::kSolid) { + std::vector physical; + for (auto boundary : boundaries) { + if (!ResolveNodeTarget(target_resolver, boundary.target, + boundary.location, "BOUNDARY")) + return; + if (boundary.last_dof > 3) { + if (boundary.value != 0.0) { + diagnostics_.InputFailure( + "unsupported-solid-rotation-boundary", boundary.location, + "BOUNDARY", boundary.target, + "Solid rotational boundaries must be exact zero to be " + "inactive."); + return; + } + definition_.warnings.push_back( + {Severity::kWarning, "inactive-solid-rotation-boundary", + boundary.location, "BOUNDARY", boundary.target, + "Exact-zero rotational boundary DOFs " + + std::to_string(std::max(4, boundary.first_dof)) + ".." + + std::to_string(boundary.last_dof) + + " are inactive; Solid equations contain translations " + "only."}); + boundary.last_dof = 3; + } + if (boundary.first_dof <= boundary.last_dof) + physical.push_back(std::move(boundary)); + } + boundaries = std::move(physical); + std::vector active(definition_.nodes.size(), false); + for (const auto& element : definition_.solid_elements) + for (const auto node : element.node_indices) active[node] = true; + for (std::size_t node = 0U; node < active.size(); ++node) { + if (!active[node]) { + diagnostics_.ModelFailure( + "unused-solid-node", definition_.nodes[node].location, "NODE", + definition_.nodes[node].source_id.source_label_text, + "Every Solid node must connect to a C3D8 element."); + return; + } + } + } + std::map, double> prescribed_values; + for (const auto& boundary : boundaries) { + auto target = ResolveNodeTarget(target_resolver, boundary.target, + boundary.location, "BOUNDARY"); + if (!target) { + return; + } + for (const auto node : *target) { + for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) { + const auto key = std::make_pair(node, dof); + const auto existing = prescribed_values.find(key); + if (existing != prescribed_values.end() && + existing->second != boundary.value) { + diagnostics_.InputFailure( + "conflicting-boundary-condition", boundary.location, "BOUNDARY", + boundary.target, + "Expanded boundary rows prescribe different values to " + "one node/DOF."); + return; + } + prescribed_values[key] = boundary.value; + } + } + } + for (const auto& load : step_.loads) { + if (!ResolveNodeTarget(target_resolver, load.target, load.location, + "CLOAD")) { + return; + } + } + if (step_.frequency) { + step_.frequency->boundaries = std::move(boundaries); + definition_.frequency_step = std::move(step_.frequency); + return; + } + if (step_.dynamic) { + step_.dynamic->boundaries = std::move(boundaries); + step_.dynamic->loads = step_.loads; + definition_.dynamic_step = std::move(step_.dynamic); + return; + } + std::vector surface_pressure_loads; + surface_pressure_loads.reserve(step_.surface_pressure_loads.size()); + for (const auto& load : step_.surface_pressure_loads) { + const auto surface = std::find_if( + definition_.element_surfaces.begin(), + definition_.element_surfaces.end(), [&load](const auto& candidate) { + return AsciiCaseInsensitiveEquals(candidate.surface_name_raw, + load.surface_name); + }); + if (surface == definition_.element_surfaces.end()) { + diagnostics_.InputFailure( + "missing-reference", load.data_location, "DSLOAD", load.surface_name, + "A DSLOAD surface must resolve after surface validation."); + return; + } + surface_pressure_loads.push_back( + {step_.name_raw, step_.name_canonical, + static_cast( + std::distance(definition_.element_surfaces.begin(), surface)), + load.surface_name, AsciiUppercase(load.surface_name), "P", + load.magnitude, load.magnitude_raw, load.keyword_location, + load.data_location, load.data_ordinal, load.load_ordinal}); + } + + // Static time-control values are provenance only: V0 creates exactly + // the canonical final frame 0 and performs no increment loop here. + definition_.steps.push_back( + {model_element_family_ == ElementFamily::kSolid ? step_.name_raw + : "Step-1", + std::move(boundaries), step_.loads, std::move(surface_pressure_loads), + step_.procedure_kind, step_.nlgeom, step_.static_values[0], + step_.static_values[1], step_.static_values[2], step_.static_values[3], + step_.location}); + definition_.steps.back().direct = step_.static_direct; + definition_.steps.back().increment_count = step_.static_increment_count; + definition_.steps.back().maximum_increments = step_.maximum_increments; +} + Status StepDefinitionMapper::Map(DomainMappingContext& context) const { return context.FinalizeStep(); } diff --git a/src/fesa/io/abaqus/surface_definition_candidates.h b/src/fesa/io/abaqus/surface_definition_candidates.h new file mode 100644 index 0000000..ce9ed9d --- /dev/null +++ b/src/fesa/io/abaqus/surface_definition_candidates.h @@ -0,0 +1,87 @@ +#ifndef FESA_IO_ABAQUS_SURFACE_DEFINITION_CANDIDATES_H_ +#define FESA_IO_ABAQUS_SURFACE_DEFINITION_CANDIDATES_H_ + +#include +#include +#include +#include +#include +#include + +#include "io/abaqus/topology_candidates.h" + +namespace fesa::abaqus_internal { + +/// @brief Checks the source name syntax shared by surface and pressure records. +bool IsPressureNameToken(std::string_view text) noexcept; + +/// @brief Owns assembly surface candidates and ordered facet resolution. +/// @note Context diagnostics, topology, family, and candidate outlive this +/// stage. +class SurfaceDefinitionCandidates { + public: + /// @brief Borrows context services while owning this uncommitted mapping + /// stage. + SurfaceDefinitionCandidates(MappingDiagnostics& diagnostics, + const TopologyCandidates& topology, + const std::optional& family, + ModelDefinition& definition) + : diagnostics_{diagnostics}, + topology_{topology}, + model_element_family_{family}, + definition_{definition} {} + + /// @brief Copies first-surface provenance for material procedure validation. + std::optional FirstSurface() const; + + /// @brief Owns one source surface after assembly-scope validation. + void ParseElementSurface(const KeywordBlock& block); + + /// @brief Resolves ordered facets while retaining first failure and warning + /// order. + void ResolveElementSurfaces(); + + private: + struct RawSurfaceMember { + std::string target; + ShellSurfaceSide side; + SourceLocation location; + std::size_t ordinal; + }; + + struct RawElementSurface { + std::string name; + SourceLocation location; + std::size_t declaration_ordinal; + std::vector members; + }; + + bool ContainsSurfaceName(const std::string& name) const; + + bool AppendSurfaceFacet( + const RawSurfaceMember& member, const RawInstance& raw_instance, + std::size_t instance_ordinal, EntityIndex element_index, + SurfaceMemberKind member_kind, const std::string& elset_name_raw, + std::vector>* + candidates); + + bool ResolveDirectSurfaceMember( + const RawSurfaceMember& member, + std::vector>* + candidates); + + bool ResolveElsetSurfaceMember( + const RawSurfaceMember& member, + std::vector>* + candidates); + + MappingDiagnostics& diagnostics_; + const TopologyCandidates& topology_; + const std::optional& model_element_family_; + ModelDefinition& definition_; + std::vector element_surfaces_; +}; + +} // namespace fesa::abaqus_internal + +#endif // FESA_IO_ABAQUS_SURFACE_DEFINITION_CANDIDATES_H_ diff --git a/src/fesa/io/abaqus/surface_definition_mapper.cpp b/src/fesa/io/abaqus/surface_definition_mapper.cpp index 0855e11..9931d7b 100644 --- a/src/fesa/io/abaqus/surface_definition_mapper.cpp +++ b/src/fesa/io/abaqus/surface_definition_mapper.cpp @@ -1,9 +1,363 @@ #include "io/abaqus/surface_definition_mapper.h" +#include +#include +#include +#include + +#include "fesa/core/ascii.h" #include "io/abaqus/domain_mapping_context.h" +#include "io/abaqus/surface_definition_candidates.h" namespace fesa::abaqus_internal { +bool IsPressureNameToken(const std::string_view text) noexcept { + if (text.empty() || text.size() > 80U) { + return false; + } + const auto is_initial = [](const char value) { + return (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || + value == '_'; + }; + const auto is_subsequent = [is_initial](const char value) { + return is_initial(value) || (value >= '0' && value <= '9') || + value == '-' || value == '_'; + }; + return is_initial(text.front()) && + std::all_of(text.begin() + 1, text.end(), is_subsequent); +} + +std::optional +SurfaceDefinitionCandidates::FirstSurface() const { + if (element_surfaces_.empty()) return std::nullopt; + return MaterialProcedureView::Surface{element_surfaces_.front().location, + element_surfaces_.front().name}; +} + +bool SurfaceDefinitionCandidates::ContainsSurfaceName( + const std::string& name) const { + return std::any_of(element_surfaces_.begin(), element_surfaces_.end(), + [&name](const RawElementSurface& surface) { + return AsciiCaseInsensitiveEquals(surface.name, name); + }); +} + +void SurfaceDefinitionCandidates::ParseElementSurface( + const KeywordBlock& block) { + if (!diagnostics_.ValidateParameters(block, {"TYPE", "NAME"}, + "unsupported-parameter")) { + return; + } + const auto* type = diagnostics_.RequiredParameterValue(block, "TYPE"); + const auto* name = diagnostics_.RequiredParameterValue(block, "NAME"); + if (type == nullptr || name == nullptr) { + return; + } + if (!IsPressureNameToken(*name)) { + diagnostics_.InputFailure( + "invalid-surface-name", block.location, block.canonical_name, *name, + "Surface names must use the approved ASCII name syntax."); + return; + } + if (!AsciiCaseInsensitiveEquals(*type, "ELEMENT")) { + diagnostics_.InputFailure( + "unsupported-surface-type", block.location, block.canonical_name, *type, + "Only TYPE=ELEMENT belongs to the approved surface subset."); + return; + } + if (ContainsSurfaceName(*name)) { + diagnostics_.InputFailure( + "duplicate-entity", block.location, block.canonical_name, *name, + "Surface names are unique in the assembly namespace."); + return; + } + if (block.data.empty()) { + diagnostics_.InputFailure( + "empty-surface", block.location, block.canonical_name, *name, + "An element surface requires at least one member."); + return; + } + + RawElementSurface surface{ + *name, block.location, element_surfaces_.size(), {}}; + for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) { + const auto& row = block.data[ordinal]; + if (row.fields.size() != 2U || row.fields[0].empty() || + row.fields[1].empty()) { + diagnostics_.InputFailure( + "invalid-data-arity", row.location, block.canonical_name, *name, + "Surface rows require one member and SPOS or SNEG."); + return; + } + ShellSurfaceSide side = ShellSurfaceSide::kSpos; + if (AsciiCaseInsensitiveEquals(row.fields[1], "SPOS")) { + side = ShellSurfaceSide::kSpos; + } else if (AsciiCaseInsensitiveEquals(row.fields[1], "SNEG")) { + side = ShellSurfaceSide::kSneg; + } else { + diagnostics_.InputFailure("unsupported-surface-side", row.location, + block.canonical_name, row.fields[1], + "Surface sides must be SPOS or SNEG."); + return; + } + surface.members.push_back({row.fields[0], side, row.location, ordinal}); + } + element_surfaces_.push_back(std::move(surface)); +} + +bool SurfaceDefinitionCandidates::AppendSurfaceFacet( + const RawSurfaceMember& member, const RawInstance& raw_instance, + std::size_t instance_ordinal, EntityIndex element_index, + SurfaceMemberKind member_kind, const std::string& elset_name_raw, + std::vector>* + candidates) { + const bool thermal = model_element_family_ == ElementFamily::kThermalShell; + const auto element_count = thermal ? definition_.thermal_elements.size() + : definition_.shell_elements.size(); + if (static_cast(element_index) >= element_count) { + return diagnostics_.InputFailure( + "unsupported-element-formulation", member.location, "SURFACE", + member.target, + "Element surfaces may resolve only to S4 or S4R shell elements."); + } + const RawPart* part = topology_.FindPart(raw_instance.part_name); + if (part == nullptr) { + return diagnostics_.InputFailure( + "unresolved-reference", member.location, "SURFACE", member.target, + "The surface instance must resolve to its source part."); + } + const auto& source_id = + thermal ? definition_.thermal_elements[element_index].source_id + : definition_.shell_elements[element_index].source_id; + const std::string source_type = + thermal ? "DS4" + : (definition_.shell_elements[element_index].source_type == + ShellSourceElementType::kS4 + ? "S4" + : "S4R"); + ElementSurfaceFacetDefinition facet{element_index, + 0U, + part->name, + AsciiUppercase(part->name), + raw_instance.name, + AsciiUppercase(raw_instance.name), + source_id.source_label, + source_id.source_label_text, + source_type, + member.side, + member.location, + member.ordinal, + member_kind, + elset_name_raw, + member_kind == SurfaceMemberKind::kElset + ? AsciiUppercase(elset_name_raw) + : ""}; + candidates->push_back({std::move(facet), instance_ordinal}); + return true; +} + +bool SurfaceDefinitionCandidates::ResolveDirectSurfaceMember( + const RawSurfaceMember& member, + std::vector>* + candidates) { + const std::size_t dot = member.target.find('.'); + if (dot == std::string::npos || dot == 0U || + dot + 1U >= member.target.size() || + member.target.find('.', dot + 1U) != std::string::npos) { + return diagnostics_.InputFailure( + "unresolved-reference", member.location, "SURFACE", member.target, + "A direct surface member must be instance.positive-element-label."); + } + const std::string instance_name = member.target.substr(0U, dot); + if (!IsPressureNameToken(instance_name)) { + return diagnostics_.InputFailure( + "unsupported-surface-target", member.location, "SURFACE", member.target, + "A direct surface member must use an identity instance name."); + } + std::int64_t source_label = 0; + if (!diagnostics_.ParseInteger(member.target.substr(dot + 1U), source_label, + member.location, "SURFACE", true)) { + return false; + } + const auto raw_instance = std::find_if( + topology_.Instances().begin(), topology_.Instances().end(), + [&instance_name](const RawInstance& value) { + return AsciiCaseInsensitiveEquals(value.name, instance_name); + }); + if (raw_instance == topology_.Instances().end()) { + if (topology_.FindPart(instance_name) != nullptr) { + return diagnostics_.InputFailure( + "unsupported-surface-target", member.location, "SURFACE", + member.target, + "A direct surface member cannot use a part name as an instance."); + } + return diagnostics_.InputFailure( + "unresolved-reference", member.location, "SURFACE", member.target, + "A direct surface member instance must resolve."); + } + const auto instance = std::find_if( + definition_.instances.begin(), definition_.instances.end(), + [&raw_instance](const InstanceDefinition& value) { + return AsciiCaseInsensitiveEquals(value.name, raw_instance->name); + }); + const auto element = std::find_if( + instance->element_mappings.begin(), instance->element_mappings.end(), + [source_label](const SourceIndexMapping& value) { + return value.source_label == source_label; + }); + if (element == instance->element_mappings.end()) { + return diagnostics_.InputFailure( + "unresolved-reference", member.location, "SURFACE", member.target, + "A direct surface member must resolve to a supported shell element."); + } + return AppendSurfaceFacet(member, *raw_instance, + static_cast(std::distance( + topology_.Instances().begin(), raw_instance)), + element->internal_index, SurfaceMemberKind::kDirect, + "", candidates); +} + +bool SurfaceDefinitionCandidates::ResolveElsetSurfaceMember( + const RawSurfaceMember& member, + std::vector>* + candidates) { + if (!IsPressureNameToken(member.target)) { + return diagnostics_.InputFailure( + "unsupported-surface-target", member.location, "SURFACE", member.target, + "A surface ELSET must use the approved name syntax."); + } + const auto raw_set = std::find_if( + topology_.AssemblySets().begin(), topology_.AssemblySets().end(), + [&member](const RawAssemblySet& value) { + return !value.is_node_set && + AsciiCaseInsensitiveEquals(value.name, member.target); + }); + if (raw_set == topology_.AssemblySets().end()) { + const bool part_scoped = std::any_of( + topology_.Parts().begin(), topology_.Parts().end(), + [this, &member](const RawPart& part) { + return FindSet(part.element_sets, member.target) != nullptr; + }); + if (part_scoped) { + return diagnostics_.InputFailure( + "cross-scope-target", member.location, "SURFACE", member.target, + "A surface ELSET must be declared at assembly scope."); + } + return diagnostics_.InputFailure( + "missing-reference", member.location, "SURFACE", member.target, + "A surface ELSET must be assembly-scoped and explicit-instance."); + } + const auto raw_instance = std::find_if( + topology_.Instances().begin(), topology_.Instances().end(), + [&raw_set](const RawInstance& value) { + return AsciiCaseInsensitiveEquals(value.name, raw_set->instance_name); + }); + if (raw_instance == topology_.Instances().end()) { + return diagnostics_.InputFailure( + "unresolved-reference", member.location, "SURFACE", member.target, + "The surface ELSET instance must resolve."); + } + const auto element_set = std::find_if( + definition_.element_sets.begin(), definition_.element_sets.end(), + [&raw_set](const ElementSet& value) { + return AsciiCaseInsensitiveEquals(value.name, raw_set->name) && + value.instance_name && + AsciiCaseInsensitiveEquals(*value.instance_name, + raw_set->instance_name); + }); + if (element_set == definition_.element_sets.end()) { + return diagnostics_.InputFailure( + "unresolved-reference", member.location, "SURFACE", member.target, + "The surface ELSET must expand in its explicit instance."); + } + for (const EntityIndex element_index : element_set->element_indices) { + if (!AppendSurfaceFacet(member, *raw_instance, + static_cast(std::distance( + topology_.Instances().begin(), raw_instance)), + element_index, SurfaceMemberKind::kElset, + member.target, candidates)) { + return false; + } + } + return true; +} + +void SurfaceDefinitionCandidates::ResolveElementSurfaces() { + std::vector resolved_surfaces; + resolved_surfaces.reserve(element_surfaces_.size()); + for (const auto& surface : element_surfaces_) { + std::vector> + candidates; + for (const auto& member : surface.members) { + const bool is_direct = member.target.find('.') != std::string::npos; + if (!(is_direct ? ResolveDirectSurfaceMember(member, &candidates) + : ResolveElsetSurfaceMember(member, &candidates))) { + return; + } + } + if (candidates.empty()) { + diagnostics_.InputFailure( + "empty-surface", surface.location, "SURFACE", surface.name, + "An element surface must resolve at least one facet."); + return; + } + + std::vector> + unique_facets; + for (auto& candidate : candidates) { + const auto same_element = + std::find_if(unique_facets.begin(), unique_facets.end(), + [&candidate](const auto& existing) { + return existing.first.instance_name_canonical == + candidate.first.instance_name_canonical && + existing.first.source_element_label == + candidate.first.source_element_label; + }); + if (same_element != unique_facets.end()) { + if (same_element->first.side != candidate.first.side) { + diagnostics_.InputFailure( + "conflicting-surface-membership", + candidate.first.first_member_location, "SURFACE", surface.name, + "One shell element cannot appear on both surface sides."); + return; + } + if (model_element_family_ != ElementFamily::kThermalShell) { + definition_.warnings.push_back( + {Severity::kWarning, "duplicate-surface-member-normalized", + candidate.first.first_member_location, "SURFACE", surface.name, + "A repeated same-side surface member was normalized once."}); + } + continue; + } + unique_facets.push_back(std::move(candidate)); + } + std::sort(unique_facets.begin(), unique_facets.end(), + [](const auto& lhs, const auto& rhs) { + const int lhs_side = + lhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1; + const int rhs_side = + rhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1; + return std::tie(lhs.second, lhs.first.source_element_label, + lhs_side) < + std::tie(rhs.second, rhs.first.source_element_label, + rhs_side); + }); + + ElementSurfaceDefinition resolved{surface.name, + AsciiUppercase(surface.name), + {}, + surface.location, + surface.declaration_ordinal}; + resolved.facets.reserve(unique_facets.size()); + for (std::size_t ordinal = 0U; ordinal < unique_facets.size(); ++ordinal) { + unique_facets[ordinal].first.facet_ordinal = ordinal; + resolved.facets.push_back(std::move(unique_facets[ordinal].first)); + } + resolved_surfaces.push_back(std::move(resolved)); + } + definition_.element_surfaces = std::move(resolved_surfaces); +} + Status SurfaceDefinitionMapper::Map(DomainMappingContext& context) const { return context.FinalizeElementSurfaces(); } diff --git a/src/fesa/io/abaqus/topology_candidates.h b/src/fesa/io/abaqus/topology_candidates.h new file mode 100644 index 0000000..c74eb87 --- /dev/null +++ b/src/fesa/io/abaqus/topology_candidates.h @@ -0,0 +1,133 @@ +#ifndef FESA_IO_ABAQUS_TOPOLOGY_CANDIDATES_H_ +#define FESA_IO_ABAQUS_TOPOLOGY_CANDIDATES_H_ + +#include +#include +#include +#include + +#include "fesa/core/source_identity.h" +#include "io/abaqus/material_property_candidates.h" + +namespace fesa::abaqus_internal { + +struct RawNode { + std::int64_t label; + std::string label_text; + std::array coordinates; + SourceLocation location; +}; + +struct RawElement { + enum class Type { kB33, kS4, kS4r, kDs4, kC3d8 }; + + std::int64_t label; + std::string label_text; + Type type; + std::vector node_labels; + SourceLocation location; +}; + +enum class ElementFamily { kBeam, kShell, kThermalShell, kSolid }; + +struct RawSet { + std::string name; + std::vector members; + SourceLocation location; +}; + +struct RawPart { + std::string name; + SourceLocation location; + std::vector nodes; + std::vector elements; + std::vector node_sets; + std::vector element_sets; + PartPropertyCandidates properties; +}; + +struct RawInstance { + std::string name; + std::string part_name; + SourceLocation location; +}; + +struct RawAssemblySet { + bool is_node_set; + std::string name; + std::string instance_name; + std::vector members; + SourceLocation location; +}; + +/// @brief Borrows a part-local set by ASCII-insensitive name or returns null. +const RawSet* FindSet(const std::vector& sets, const std::string& name); + +/// @brief Borrows a part-local source node by label or returns null. +const RawNode* FindNode(const RawPart& part, std::int64_t label); + +/// @brief Borrows a part-local source element by label or returns null. +const RawElement* FindElement(const RawPart& part, std::int64_t label); + +/// @brief Owns raw topology and its deterministic expansion into a candidate. +/// @note The syntax coordinator may mutate raw records until finalization. +/// Context-owned diagnostics outlive this stage and its borrowed consumers. +class TopologyCandidates { + public: + /// @brief Borrows context services while owning this uncommitted mapping + /// stage. + explicit TopologyCandidates(MappingDiagnostics& diagnostics) + : diagnostics_{diagnostics} {} + + /// @brief Provides raw part records to mapping stages and the syntax + /// coordinator. + std::vector& Parts() noexcept { return parts_; } + /// @brief Provides raw part records to mapping stages and the syntax + /// coordinator. + const std::vector& Parts() const noexcept { return parts_; } + /// @brief Provides raw identity instances in source declaration order. + std::vector& Instances() noexcept { return instances_; } + /// @brief Provides raw identity instances in source declaration order. + const std::vector& Instances() const noexcept { + return instances_; + } + /// @brief Provides raw assembly sets in source declaration order. + std::vector& AssemblySets() noexcept { + return assembly_sets_; + } + /// @brief Provides raw assembly sets in source declaration order. + const std::vector& AssemblySets() const noexcept { + return assembly_sets_; + } + + /// @brief Borrows the matching source part or returns null. + const RawPart* FindPart(const std::string& name) const; + + /// @brief Borrows the matching identity instance or returns null. + const RawInstance* FindInstance(const std::string& name) const; + + /// @brief Expands instances, preprocesses shell geometry, then expands + /// assembly sets. + void Finalize(const MaterialPropertyCandidates& material_properties, + ModelDefinition& definition); + + private: + std::size_t PartIndex(const RawPart& part) const; + + bool ValidateGeometry(const RawElement& raw, const Node& first, + const Node& second, const GeneralBeamSection& section); + + void ExpandInstances(const MaterialPropertyCandidates& material_properties, + ModelDefinition& definition); + + void ExpandAssemblySets(ModelDefinition& definition); + + MappingDiagnostics& diagnostics_; + std::vector parts_; + std::vector instances_; + std::vector assembly_sets_; +}; + +} // namespace fesa::abaqus_internal + +#endif // FESA_IO_ABAQUS_TOPOLOGY_CANDIDATES_H_ diff --git a/src/fesa/io/abaqus/topology_mapper.cpp b/src/fesa/io/abaqus/topology_mapper.cpp index 5d78175..b48e045 100644 --- a/src/fesa/io/abaqus/topology_mapper.cpp +++ b/src/fesa/io/abaqus/topology_mapper.cpp @@ -1,9 +1,414 @@ #include "io/abaqus/topology_mapper.h" +#include +#include +#include +#include +#include + +#include "fesa/core/ascii.h" +#include "fesa/math/vector3.h" +#include "fesa/model/shell_geometry.h" #include "io/abaqus/domain_mapping_context.h" +#include "io/abaqus/topology_candidates.h" namespace fesa::abaqus_internal { +const RawSet* FindSet(const std::vector& sets, + const std::string& name) { + const auto found = + std::find_if(sets.begin(), sets.end(), [&name](const RawSet& set) { + return AsciiCaseInsensitiveEquals(set.name, name); + }); + return found == sets.end() ? nullptr : &*found; +} + +const RawNode* FindNode(const RawPart& part, std::int64_t label) { + const auto found = std::find_if( + part.nodes.begin(), part.nodes.end(), + [label](const RawNode& node) { return node.label == label; }); + return found == part.nodes.end() ? nullptr : &*found; +} + +const RawElement* FindElement(const RawPart& part, std::int64_t label) { + const auto found = std::find_if( + part.elements.begin(), part.elements.end(), + [label](const RawElement& element) { return element.label == label; }); + return found == part.elements.end() ? nullptr : &*found; +} + +const RawPart* TopologyCandidates::FindPart(const std::string& name) const { + const auto found = + std::find_if(parts_.begin(), parts_.end(), [&name](const RawPart& part) { + return AsciiCaseInsensitiveEquals(part.name, name); + }); + return found == parts_.end() ? nullptr : &*found; +} + +const RawInstance* TopologyCandidates::FindInstance( + const std::string& name) const { + const auto found = + std::find_if(instances_.begin(), instances_.end(), + [&name](const RawInstance& instance) { + return AsciiCaseInsensitiveEquals(instance.name, name); + }); + return found == instances_.end() ? nullptr : &*found; +} + +std::size_t TopologyCandidates::PartIndex(const RawPart& part) const { + return static_cast(&part - parts_.data()); +} + +bool TopologyCandidates::ValidateGeometry(const RawElement& raw, + const Node& first, const Node& second, + const GeneralBeamSection& section) { + const auto maximum_absolute = [](const Vector3& vector) { + return std::max( + {std::abs(vector[0]), std::abs(vector[1]), std::abs(vector[2])}); + }; + const Vector3 first_position{first.coordinates}; + const Vector3 second_position{second.coordinates}; + + // Compare both approved inequalities after a common scaling. This + // preserves the exact ratios while avoiding overflow in x*x and in + // subtraction between large finite coordinates. + const double global_coordinate_scale = + std::max({1.0, maximum_absolute(first_position), + maximum_absolute(second_position)}); + const Vector3 first_scaled = first_position / global_coordinate_scale; + const Vector3 second_scaled = second_position / global_coordinate_scale; + const Vector3 delta_scaled = second_scaled - first_scaled; + const double length_ratio = delta_scaled.Norm(); + const double coordinate_norm_ratio = + std::max({1.0 / global_coordinate_scale, first_scaled.Norm(), + second_scaled.Norm()}); + if (!(length_ratio > 1.0e-12 * coordinate_norm_ratio)) { + return diagnostics_.ModelFailure( + "invalid-beam-length", raw.location, "ELEMENT", raw.label_text, + "Beam length fails the approved scale-aware threshold."); + } + const Vector3 tangent = delta_scaled / length_ratio; + + const Vector3 guide{section.first_axis}; + const double global_guide_scale = std::max(1.0, maximum_absolute(guide)); + const Vector3 guide_scaled = guide / global_guide_scale; + const double projection = guide_scaled.Dot(tangent); + const Vector3 perpendicular = guide_scaled - projection * tangent; + const double guide_norm_ratio = + std::max(1.0 / global_guide_scale, guide_scaled.Norm()); + if (!(perpendicular.Norm() > 1.0e-12 * guide_norm_ratio)) { + return diagnostics_.ModelFailure( + "invalid-beam-guide-vector", section.location, "BEAM GENERAL SECTION", + raw.label_text, + "The first section axis cannot be zero or tangent-parallel."); + } + return true; +} + +void TopologyCandidates::ExpandInstances( + const MaterialPropertyCandidates& material_properties, + ModelDefinition& definition) { + for (const auto& raw_instance : instances_) { + const RawPart* part = FindPart(raw_instance.part_name); + if (part == nullptr) { + diagnostics_.InputFailure( + "unresolved-reference", raw_instance.location, "INSTANCE", + raw_instance.name, "INSTANCE PART must resolve case-insensitively."); + return; + } + InstanceDefinition instance{ + raw_instance.name, part->name, {}, {}, raw_instance.location}; + std::map node_indices; + std::map element_indices; + + // Instance order, followed by part-local declaration order, is the + // sole source of stable expanded internal IDs. + for (const auto& raw_node : part->nodes) { + if (definition.nodes.size() > std::numeric_limits::max()) { + diagnostics_.InputFailure( + "entity-index-overflow", raw_node.location, "NODE", + raw_node.label_text, + "Expanded node count exceeds EntityIndex capacity."); + return; + } + const EntityIndex index = + static_cast(definition.nodes.size()); + definition.nodes.push_back( + {{raw_instance.name, raw_node.label, raw_node.label_text}, + raw_node.coordinates, + raw_node.location}); + node_indices.emplace(raw_node.label, index); + instance.node_mappings.push_back({raw_node.label, index}); + } + + for (const auto& raw_element : part->elements) { + EntityIndex index = 0U; + if (raw_element.type == RawElement::Type::kB33) { + const auto first = node_indices.find(raw_element.node_labels[0]); + const auto second = node_indices.find(raw_element.node_labels[1]); + const auto& assignments = + material_properties.BeamAssignments(PartIndex(*part)); + const auto assignment = assignments.find(raw_element.label); + if (first == node_indices.end() || second == node_indices.end() || + assignment == assignments.end()) { + diagnostics_.InputFailure( + "unresolved-reference", raw_element.location, "ELEMENT", + raw_element.label_text, + "Expanded connectivity and section assignment must resolve."); + return; + } + if (definition.elements.size() > + std::numeric_limits::max()) { + diagnostics_.InputFailure( + "entity-index-overflow", raw_element.location, "ELEMENT", + raw_element.label_text, + "Expanded element count exceeds EntityIndex capacity."); + return; + } + index = static_cast(definition.elements.size()); + const auto& section = definition.sections[assignment->second.first]; + if (!ValidateGeometry(raw_element, definition.nodes[first->second], + definition.nodes[second->second], section)) { + return; + } + definition.elements.push_back( + {{raw_instance.name, raw_element.label, raw_element.label_text}, + {first->second, second->second}, + assignment->second.second, + assignment->second.first, + raw_element.location}); + } else if (raw_element.type == RawElement::Type::kC3d8) { + std::array connected_nodes{}; + for (std::size_t node = 0U; node < connected_nodes.size(); ++node) { + const auto found = node_indices.find(raw_element.node_labels[node]); + if (found == node_indices.end()) { + diagnostics_.InputFailure( + "invalid-solid-connectivity", raw_element.location, "ELEMENT", + raw_element.label_text, "Solid nodes must resolve."); + return; + } + connected_nodes[node] = found->second; + } + const auto& assignments = + material_properties.ShellAssignments(PartIndex(*part)); + const auto assignment = assignments.find(raw_element.label); + if (assignment == assignments.end() || + definition.solid_elements.size() > + std::numeric_limits::max()) { + diagnostics_.InputFailure("invalid-solid-section-assignment", + raw_element.location, "ELEMENT", + raw_element.label_text, + "Solid assignment must resolve."); + return; + } + index = static_cast(definition.solid_elements.size()); + definition.solid_elements.emplace_back( + SourceEntityId{raw_instance.name, raw_element.label, + raw_element.label_text}, + connected_nodes, assignment->second.second, + assignment->second.first, raw_element.location); + } else { + std::array connected_nodes{}; + for (std::size_t node = 0U; node < connected_nodes.size(); ++node) { + const auto found = node_indices.find(raw_element.node_labels[node]); + if (found == node_indices.end()) { + diagnostics_.InputFailure( + "invalid-shell-connectivity", raw_element.location, "ELEMENT", + raw_element.label_text, + "Expanded shell connectivity must resolve."); + return; + } + connected_nodes[node] = found->second; + } + const auto& assignments = + material_properties.ShellAssignments(PartIndex(*part)); + const auto assignment = assignments.find(raw_element.label); + if (assignment == assignments.end()) { + diagnostics_.InputFailure( + "invalid-shell-section-assignment", raw_element.location, + "ELEMENT", raw_element.label_text, + "Expanded shell section assignment must resolve exactly once."); + return; + } + if (raw_element.type == RawElement::Type::kDs4) { + if (definition.thermal_elements.size() > + std::numeric_limits::max()) { + diagnostics_.InputFailure( + "entity-index-overflow", raw_element.location, "ELEMENT", + raw_element.label_text, + "Expanded thermal element count exceeds EntityIndex " + "capacity."); + return; + } + index = static_cast(definition.thermal_elements.size()); + definition.thermal_elements.push_back( + {{raw_instance.name, raw_element.label, raw_element.label_text}, + connected_nodes, + assignment->second.first, + raw_element.location}); + element_indices.emplace(raw_element.label, index); + instance.element_mappings.push_back({raw_element.label, index}); + continue; + } + if (definition.shell_elements.size() > + std::numeric_limits::max()) { + diagnostics_.InputFailure( + "entity-index-overflow", raw_element.location, "ELEMENT", + raw_element.label_text, + "Expanded shell element count exceeds EntityIndex capacity."); + return; + } + index = static_cast(definition.shell_elements.size()); + definition.shell_elements.push_back( + {{raw_instance.name, raw_element.label, raw_element.label_text}, + raw_element.type == RawElement::Type::kS4 + ? ShellSourceElementType::kS4 + : ShellSourceElementType::kS4r, + connected_nodes, + assignment->second.second, + assignment->second.first, + raw_element.location}); + } + element_indices.emplace(raw_element.label, index); + instance.element_mappings.push_back({raw_element.label, index}); + } + + for (const auto& raw_set : part->node_sets) { + NodeSet set{raw_set.name, raw_instance.name, {}, raw_set.location}; + for (const auto member : raw_set.members) { + const auto found = node_indices.find(member); + if (found == node_indices.end()) { + diagnostics_.InputFailure("unresolved-reference", raw_set.location, + "NSET", raw_set.name, + "Part node-set expansion failed."); + return; + } + set.node_indices.push_back(found->second); + } + definition.node_sets.push_back(std::move(set)); + } + for (const auto& raw_set : part->element_sets) { + ElementSet set{raw_set.name, raw_instance.name, {}, raw_set.location}; + for (const auto member : raw_set.members) { + const auto found = element_indices.find(member); + if (found == element_indices.end()) { + diagnostics_.InputFailure("unresolved-reference", raw_set.location, + "ELSET", raw_set.name, + "Part element-set expansion failed."); + return; + } + set.element_indices.push_back(found->second); + } + definition.element_sets.push_back(std::move(set)); + } + definition.instances.push_back(std::move(instance)); + } +} + +void TopologyCandidates::ExpandAssemblySets(ModelDefinition& definition) { + for (const auto& raw_set : assembly_sets_) { + const RawInstance* raw_instance = FindInstance(raw_set.instance_name); + if (raw_instance == nullptr) { + diagnostics_.InputFailure("unresolved-reference", raw_set.location, + raw_set.is_node_set ? "NSET" : "ELSET", + raw_set.name, + "Assembly set INSTANCE must resolve."); + return; + } + const auto definition_instance = std::find_if( + definition.instances.begin(), definition.instances.end(), + [&raw_instance](const InstanceDefinition& instance) { + return AsciiCaseInsensitiveEquals(instance.name, raw_instance->name); + }); + if (definition_instance == definition.instances.end()) { + diagnostics_.InputFailure( + "unresolved-reference", raw_set.location, + raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name, + "Assembly set instance expansion is unavailable."); + return; + } + if (raw_set.is_node_set) { + definition.node_sets.erase( + std::remove_if( + definition.node_sets.begin(), definition.node_sets.end(), + [&raw_set](const NodeSet& set) { + return AsciiCaseInsensitiveEquals(set.name, raw_set.name); + }), + definition.node_sets.end()); + NodeSet set{ + raw_set.name, definition_instance->name, {}, raw_set.location}; + for (const auto member : raw_set.members) { + const auto mapping = + std::find_if(definition_instance->node_mappings.begin(), + definition_instance->node_mappings.end(), + [member](const SourceIndexMapping& value) { + return value.source_label == member; + }); + if (mapping == definition_instance->node_mappings.end()) { + diagnostics_.InputFailure( + "unresolved-reference", raw_set.location, "NSET", raw_set.name, + "Assembly node-set member must resolve in its " + "instance."); + return; + } + set.node_indices.push_back(mapping->internal_index); + } + definition.node_sets.push_back(std::move(set)); + } else { + definition.element_sets.erase( + std::remove_if( + definition.element_sets.begin(), definition.element_sets.end(), + [&raw_set](const ElementSet& set) { + return AsciiCaseInsensitiveEquals(set.name, raw_set.name); + }), + definition.element_sets.end()); + ElementSet set{ + raw_set.name, definition_instance->name, {}, raw_set.location}; + for (const auto member : raw_set.members) { + const auto mapping = + std::find_if(definition_instance->element_mappings.begin(), + definition_instance->element_mappings.end(), + [member](const SourceIndexMapping& value) { + return value.source_label == member; + }); + if (mapping == definition_instance->element_mappings.end()) { + diagnostics_.InputFailure( + "unresolved-reference", raw_set.location, "ELSET", raw_set.name, + "Assembly element-set member must resolve in " + "its instance."); + return; + } + set.element_indices.push_back(mapping->internal_index); + } + definition.element_sets.push_back(std::move(set)); + } + } +} + +void TopologyCandidates::Finalize( + const MaterialPropertyCandidates& material_properties, + ModelDefinition& definition) { + if (diagnostics_.HasFailure()) { + return; + } + ExpandInstances(material_properties, definition); + if (!diagnostics_.HasFailure() && !definition.shell_elements.empty()) { + auto geometry = PreprocessShellGeometry( + definition.nodes, definition.shell_elements, definition.shell_sections); + if (!geometry.HasValue()) { + const auto& status = geometry.GetStatus(); + diagnostics_.RecordFailure(status); + } else { + definition.shell_node_initial_frames = + std::move(geometry.Value().nodal_frames); + } + } + if (!diagnostics_.HasFailure()) { + ExpandAssemblySets(definition); + } + return; +} + Status TopologyMapper::Map(DomainMappingContext& context) const { return context.ParseSyntax(); } diff --git a/src/fesa/io/hdf5/dynamic_hdf5_results_writer.cpp b/src/fesa/io/hdf5/dynamic_hdf5_results_writer.cpp index 19ec6c7..db70de4 100644 --- a/src/fesa/io/hdf5/dynamic_hdf5_results_writer.cpp +++ b/src/fesa/io/hdf5/dynamic_hdf5_results_writer.cpp @@ -12,7 +12,6 @@ #include "fesa/analysis/analysis_model.h" #include "fesa/build_info.h" #include "fesa/core/diagnostic.h" -#include "fesa/elements/finite_rotation.h" #include "fesa/fem/dof_manager.h" #include "io/hdf5/hdf5_atomic_file.h" #include "io/hdf5/hdf5_model_writer.h" @@ -24,6 +23,10 @@ namespace { using hdf5_internal::Hdf5Failure; using hdf5_internal::Hdf5Handle; +using hdf5_internal::CopyVectorValues; +using hdf5_internal::FlattenNodalTriads; +using hdf5_internal::FlattenNodalDirectors; +using hdf5_internal::SpatialReactionValues; struct AcceptedFrameRow { std::uint64_t frame_index; @@ -228,11 +231,6 @@ Status Failure(const char* code, const std::string& message) { return hdf5_internal::OutputFailure(code, message); } -/// @brief Copies one nonempty full-space vector in node-major order. -std::vector Values(const Vector& values) { - return std::vector{values.Data(), values.Data() + values.Size()}; -} - /// @brief Removes inherited metadata before applying the dynamic schema. void DeleteAttribute(const hid_t object, const char* name) { if (H5Aexists(object, name) > 0) { @@ -401,26 +399,6 @@ void ProcessDataset(const hid_t file, const std::string& path, } } -/// @brief Flattens nodal triad axes in A,B,D row order. -std::vector FlattenTriad(const DynamicAcceptedFrameState& frame) { - std::vector values; - values.reserve(frame.nodal_triad.size() * 9U); - for (const auto& triad : frame.nodal_triad) { - for (const auto& axis : triad) - values.insert(values.end(), axis.begin(), axis.end()); - } - return values; -} - -/// @brief Flattens nodal directors in stable node order. -std::vector FlattenDirector(const DynamicAcceptedFrameState& frame) { - std::vector values; - values.reserve(frame.nodal_director.size() * 3U); - for (const auto& director : frame.nodal_director) - values.insert(values.end(), director.begin(), director.end()); - return values; -} - /// @brief Flattens physical recovery in element and Gauss order. std::vector FlattenShell(const DynamicAcceptedFrameState& frame, const int quantity) { @@ -442,27 +420,6 @@ std::vector FlattenShell(const DynamicAcceptedFrameState& frame, return values; } -/// @brief Maps generalized support moments to work-conjugate spatial moments. -std::vector SpatialReaction(const DynamicAcceptedFrameState& frame) { - std::vector values = Values(frame.reaction); - for (std::size_t node = 0U; node < frame.nodal_internal_order.size(); - ++node) { - const std::size_t offset = node * 6U; - const Vector3 phi{frame.generalized_coordinates[offset + 3U], - frame.generalized_coordinates[offset + 4U], - frame.generalized_coordinates[offset + 5U]}; - const Matrix transform = InverseTransposeLeftJacobianSO3(phi); - for (std::size_t row = 0U; row < 3U; ++row) { - values[offset + 3U + row] = 0.0; - for (std::size_t column = 0U; column < 3U; ++column) { - values[offset + 3U + row] += - transform(row, column) * frame.reaction[offset + 3U + column]; - } - } - } - return values; -} - /// @brief Writes or reads back all mandatory accepted-endpoint quantities. void WriteFrame(const hid_t file, const Domain& domain, const DynamicAcceptedFrameState& frame, @@ -491,52 +448,55 @@ void WriteFrame(const hid_t file, const Domain& domain, "force,force,force,force*length,force*length,force*length"; const std::vector nodal{nodes, 6U}; WriteDataset(file, root + "/nodal/displacement", nodal, - Values(frame.generalized_coordinates), "UX,UY,UZ,URX,URY,URZ", + CopyVectorValues(frame.generalized_coordinates), "UX,UY,UZ,URX,URY,URZ", "length,length,length,1,1,1", "global-cartesian", "nodal", frame); - WriteDataset(file, root + "/nodal/velocity", nodal, Values(frame.velocity), + WriteDataset(file, root + "/nodal/velocity", nodal, CopyVectorValues(frame.velocity), "V1,V2,V3,PHIDOT1,PHIDOT2,PHIDOT3", "length/time,length/time,length/time,1/time,1/time,1/time", "fixed-initial-global-chart", "nodal", frame); WriteDataset( - file, root + "/nodal/acceleration", nodal, Values(frame.acceleration), + file, root + "/nodal/acceleration", nodal, CopyVectorValues(frame.acceleration), "A1,A2,A3,PHIDDOT1,PHIDDOT2,PHIDDOT3", "length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2", "fixed-initial-global-chart", "nodal", frame); WriteDataset( file, root + "/nodal/algorithmic_acceleration", nodal, - Values(frame.algorithmic_acceleration), "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6", + CopyVectorValues(frame.algorithmic_acceleration), "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6", "length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2", "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/angular_velocity", {nodes, 3U}, - Values(frame.angular_velocity), "OMEGA1,OMEGA2,OMEGA3", + CopyVectorValues(frame.angular_velocity), "OMEGA1,OMEGA2,OMEGA3", "1/time,1/time,1/time", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/angular_acceleration", {nodes, 3U}, - Values(frame.angular_acceleration), + CopyVectorValues(frame.angular_acceleration), "OMEGADOT1,OMEGADOT2,OMEGADOT3", "1/time^2,1/time^2,1/time^2", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/triad", {nodes, 3U, 3U}, - FlattenTriad(frame), "X,Y,Z", "1,1,1", "global-cartesian", + FlattenNodalTriads(frame.nodal_triad), "X,Y,Z", "1,1,1", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/director", {nodes, 3U}, - FlattenDirector(frame), "D1,D2,D3", "1,1,1", "global-cartesian", + FlattenNodalDirectors(frame.nodal_director), "D1,D2,D3", "1,1,1", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_inertia_force", nodal, - Values(frame.inertia_force), kGeneralized, kGeneralizedUnits, + CopyVectorValues(frame.inertia_force), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_internal_force", nodal, - Values(frame.internal_force), kGeneralized, kGeneralizedUnits, + CopyVectorValues(frame.internal_force), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_external_force", nodal, - Values(frame.external_force), kGeneralized, kGeneralizedUnits, + CopyVectorValues(frame.external_force), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_residual", nodal, - Values(frame.residual), kGeneralized, kGeneralizedUnits, + CopyVectorValues(frame.residual), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_reaction", nodal, - Values(frame.reaction), kGeneralized, kGeneralizedUnits, + CopyVectorValues(frame.reaction), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); - WriteDataset(file, root + "/nodal/reaction", nodal, SpatialReaction(frame), + WriteDataset(file, root + "/nodal/reaction", nodal, + SpatialReactionValues(frame.generalized_coordinates, + frame.reaction, + frame.nodal_internal_order.size()), "RF1,RF2,RF3,RM1,RM2,RM3", kGeneralizedUnits, "global-cartesian", "nodal", frame); const std::string shell = root + "/element/shell/"; @@ -810,13 +770,7 @@ void CheckModel(const hid_t file, const Domain& domain, model.prescribed_displacement); std::vector directors; std::vector frames; - for (const auto& frame : domain.ShellNodeInitialFrames()) { - directors.insert(directors.end(), frame.director.begin(), - frame.director.end()); - frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end()); - frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end()); - frames.insert(frames.end(), frame.director.begin(), frame.director.end()); - } + hdf5_internal::AppendShellReferenceFrames(domain, directors, frames); (void)CheckDoubles(file, "/model/shell/nodal_director", {count, 3U}, directors); (void)CheckDoubles(file, "/model/shell/nodal_frame", {count, 3U, 3U}, frames); diff --git a/src/fesa/io/hdf5/eigen_hdf5_results_writer.cpp b/src/fesa/io/hdf5/eigen_hdf5_results_writer.cpp index cbcfa5b..60688de 100644 --- a/src/fesa/io/hdf5/eigen_hdf5_results_writer.cpp +++ b/src/fesa/io/hdf5/eigen_hdf5_results_writer.cpp @@ -20,23 +20,16 @@ #include "io/hdf5/hdf5_model_writer.h" #include "io/hdf5/hdf5_primitives.h" #include "io/hdf5/hdf5_raii.h" +#include "math/finite_values.h" namespace fesa { namespace { -constexpr const char* kEigenRoot = "/steps/Step-1/eigen"; +using math_internal::IsFinite; -void WriteQuantityAttributes(hid_t dataset, const std::string& components, - const std::string& units, - const std::string& coordinates, - const std::string& location) { - hdf5_internal::WriteStringAttribute(dataset, "component_names", components); - hdf5_internal::WriteStringAttribute(dataset, "component_unit_dimensions", - units); - hdf5_internal::WriteStringAttribute(dataset, "coordinate_system", - coordinates); - hdf5_internal::WriteStringAttribute(dataset, "location", location); -} +using hdf5_internal::WriteQuantityAttributes; + +constexpr const char* kEigenRoot = "/steps/Step-1/eigen"; void WriteModalAttributes(hid_t dataset, const std::string& source_step) { hdf5_internal::WriteUint64Attribute(dataset, "eigen_schema_version", 1U); @@ -44,17 +37,6 @@ void WriteModalAttributes(hid_t dataset, const std::string& source_step) { hdf5_internal::WriteStringAttribute(dataset, "domain", "mode"); } -bool IsFinite(const Matrix& matrix) { - for (std::size_t row = 0U; row < matrix.Rows(); ++row) { - for (std::size_t column = 0U; column < matrix.Columns(); ++column) { - if (!std::isfinite(matrix(row, column))) { - return false; - } - } - } - return true; -} - Status Validate(const Domain& domain, const EigenAnalysisState& state, const std::vector& diagnostics) { const FrequencyStepDefinition* frequency = domain.Frequency(); diff --git a/src/fesa/io/hdf5/eigen_hdf5_self_check.cpp b/src/fesa/io/hdf5/eigen_hdf5_self_check.cpp index c54c43e..b41907e 100644 --- a/src/fesa/io/hdf5/eigen_hdf5_self_check.cpp +++ b/src/fesa/io/hdf5/eigen_hdf5_self_check.cpp @@ -489,13 +489,7 @@ bool CheckModelRows(hid_t file, const Domain& domain) { [&](auto i) { return domain.ShellSections()[i].thickness; })) return false; std::vector directors, frames; - for (const auto& frame : domain.ShellNodeInitialFrames()) { - directors.insert(directors.end(), frame.director.begin(), - frame.director.end()); - frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end()); - frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end()); - frames.insert(frames.end(), frame.director.begin(), frame.director.end()); - } + hdf5_internal::AppendShellReferenceFrames(domain, directors, frames); return CheckArray(file, "/model/shell/nodal_director", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, {static_cast(domain.Nodes().size()), 3U}, diff --git a/src/fesa/io/hdf5/hdf5_model_writer.cpp b/src/fesa/io/hdf5/hdf5_model_writer.cpp index 628c331..91e83f5 100644 --- a/src/fesa/io/hdf5/hdf5_model_writer.cpp +++ b/src/fesa/io/hdf5/hdf5_model_writer.cpp @@ -419,13 +419,7 @@ void WriteShellModelData(const hid_t file, const Domain& domain, directors.reserve(domain.Nodes().size() * 3U); std::vector frames; frames.reserve(domain.Nodes().size() * 9U); - for (const auto& frame : domain.ShellNodeInitialFrames()) { - directors.insert(directors.end(), frame.director.begin(), - frame.director.end()); - frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end()); - frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end()); - frames.insert(frames.end(), frame.director.begin(), frame.director.end()); - } + hdf5_internal::AppendShellReferenceFrames(domain, directors, frames); WriteModelDoubleDataset(file, "/model/shell/nodal_director", {static_cast(domain.Nodes().size()), 3U}, directors.data(), directors.size(), "D1,D2,D3", diff --git a/src/fesa/io/hdf5/hdf5_primitives.cpp b/src/fesa/io/hdf5/hdf5_primitives.cpp index 1256580..7a872b8 100644 --- a/src/fesa/io/hdf5/hdf5_primitives.cpp +++ b/src/fesa/io/hdf5/hdf5_primitives.cpp @@ -5,9 +5,13 @@ #include #include #include +#include #include #include +#include "fesa/elements/finite_rotation.h" +#include "fesa/math/vector3.h" + namespace fesa::hdf5_internal { Status OutputFailure(const std::string& code, const std::string& message) { @@ -60,10 +64,20 @@ bool IsValidUtf8(const std::string& value) { return true; } -bool SameIdentity(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; +bool SameIdentity(const SourceEntityId& left, + const SourceEntityId& right) { + return fesa::SourceIdentityEquals(left, right); +} + +void AppendShellReferenceFrames(const Domain& domain, + std::vector& directors, + std::vector& frames) { + for (const auto& frame : domain.ShellNodeInitialFrames()) { + directors.insert(directors.end(), frame.director.begin(), frame.director.end()); + frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end()); + frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end()); + frames.insert(frames.end(), frame.director.begin(), frame.director.end()); + } } bool IsShellDomain(const Domain& domain) noexcept { @@ -95,6 +109,55 @@ std::string SourceInputIdentity(const Domain& domain) { ";content_identity=" + domain.SourceContentIdentity(); } +std::vector CopyVectorValues(const Vector& source) { + std::vector values(source.Size()); + for (std::size_t index = 0U; index < source.Size(); ++index) { + values[index] = source[index]; + } + return values; +} + +std::vector FlattenNodalTriads( + const std::vector, 3>>& triads) { + std::vector values; + values.reserve(triads.size() * 9U); + for (const auto& triad : triads) { + for (const auto& axis : triad) { + values.insert(values.end(), axis.begin(), axis.end()); + } + } + return values; +} + +std::vector FlattenNodalDirectors( + const std::vector>& directors) { + std::vector values; + values.reserve(directors.size() * 3U); + for (const auto& director : directors) { + values.insert(values.end(), director.begin(), director.end()); + } + return values; +} + +std::vector SpatialReactionValues(const Vector& rotation, + const Vector& reaction, + const std::size_t node_count) { + std::vector values = CopyVectorValues(reaction); + for (std::size_t node = 0U; node < node_count; ++node) { + const std::size_t offset = kDofsPerNode * node; + const Vector3 phi{rotation[offset + 3U], rotation[offset + 4U], + rotation[offset + 5U]}; + const Vector3 generalized{reaction[offset + 3U], reaction[offset + 4U], + reaction[offset + 5U]}; + const auto moment = SpatialMomentSO3(phi, generalized); + if (!moment.HasValue()) + throw std::runtime_error{"Spatial reaction moment is nonfinite."}; + for (std::size_t row = 0U; row < 3U; ++row) + values[offset + 3U + row] = moment.Value()[row]; + } + return values; +} + Hdf5Handle MakeUtf8StringType() { Hdf5Handle type{ RequireHdf5Id(H5Tcopy(H5T_C_S1), "Unable to copy an HDF5 string type."), @@ -186,7 +249,7 @@ Hdf5Handle CreateDatasetSpace(const std::vector& dimensions) { H5Sclose}; } -void WriteResultAttributes(const hid_t dataset, +void WriteQuantityAttributes(const hid_t dataset, const std::string& component_names, const std::string& component_units, const std::string& coordinate_system, @@ -195,6 +258,15 @@ void WriteResultAttributes(const hid_t dataset, WriteStringAttribute(dataset, "component_unit_dimensions", component_units); WriteStringAttribute(dataset, "coordinate_system", coordinate_system); WriteStringAttribute(dataset, "location", location); +} + +void WriteResultAttributes(const hid_t dataset, + const std::string& component_names, + const std::string& component_units, + const std::string& coordinate_system, + const std::string& location) { + WriteQuantityAttributes(dataset, component_names, component_units, + coordinate_system, location); WriteStringAttribute(dataset, "step_name", kStepName); WriteUint64Attribute(dataset, "frame_index", 0U); } @@ -238,11 +310,8 @@ void WriteModelDoubleDataset(const hid_t file, const std::string& path, const std::string& coordinate_system, const std::string& location) { auto dataset = WriteDoubleValues(file, path, dimensions, values, value_count); - WriteStringAttribute(dataset.Get(), "component_names", component_names); - WriteStringAttribute(dataset.Get(), "component_unit_dimensions", - component_units); - WriteStringAttribute(dataset.Get(), "coordinate_system", coordinate_system); - WriteStringAttribute(dataset.Get(), "location", location); + WriteQuantityAttributes(dataset.Get(), component_names, component_units, + coordinate_system, location); } void WriteUint8Dataset(const hid_t file, const std::string& path, diff --git a/src/fesa/io/hdf5/hdf5_primitives.h b/src/fesa/io/hdf5/hdf5_primitives.h index da215e4..dcca53e 100644 --- a/src/fesa/io/hdf5/hdf5_primitives.h +++ b/src/fesa/io/hdf5/hdf5_primitives.h @@ -13,11 +13,15 @@ #include "fesa/core/source_identity.h" #include "fesa/core/status.h" +#include "fesa/math/vector.h" #include "fesa/model/domain.h" #include "io/hdf5/hdf5_raii.h" +#include "math/finite_values.h" namespace fesa::hdf5_internal { +using math_internal::IsFinite; + inline constexpr std::size_t kDofsPerNode = 6U; inline constexpr std::size_t kEndpointCount = 2U; inline constexpr std::size_t kGaussPointCount = 2U; @@ -64,16 +68,11 @@ Status OutputFailure(const std::string& code, const std::string& message); /// @brief Tests exact preserved source identity. bool SameIdentity(const SourceEntityId& left, const SourceEntityId& right); -/// @brief Tests a fixed component record for finite values. -template -bool IsFinite(const std::array& values) { - for (const double value : values) { - if (!std::isfinite(value)) { - return false; - } - } - return true; -} +/// @brief Appends source-ordered reference directors and a,b,d frame columns. +/// @note Callers retain allocation, schema, write/readback and failure ownership. +void AppendShellReferenceFrames(const Domain& domain, + std::vector& directors, + std::vector& frames); /// @brief Validates UTF-8 without accepting overlong or surrogate encodings. bool IsValidUtf8(const std::string& value); @@ -93,6 +92,25 @@ std::string NormalizedPathString(const std::filesystem::path& path); /// @brief Returns schema-v0 source path and content identity metadata. std::string SourceInputIdentity(const Domain& domain); +/// @brief Copies validated vector values in their existing component order. +std::vector CopyVectorValues(const Vector& source); + +/// @brief Flattens global nodal A,B,D axes in stable node/axis/component order. +std::vector FlattenNodalTriads( + const std::vector, 3>>& triads); + +/// @brief Flattens global nodal directors in stable node/component order. +std::vector FlattenNodalDirectors( + const std::vector>& directors); + +/// @brief Preserves force rows and maps generalized moments to spatial moments. +/// @pre Both six-DOF vectors and node_count were validated by the typed writer. +/// @note Rotation entries are at offsets 3..5; conversion uses J^T m = q. +/// @throws std::runtime_error when the existing spatial-moment solve fails. +std::vector SpatialReactionValues(const Vector& rotation, + const Vector& reaction, + std::size_t node_count); + Hdf5Handle MakeUtf8StringType(); void WriteStringAttribute(hid_t object, const char* name, const std::string& value); @@ -101,6 +119,11 @@ void WriteUint8Attribute(hid_t object, const char* name, std::uint8_t value); void WriteDoubleAttribute(hid_t object, const char* name, double value); Hdf5Handle CreateGroup(hid_t file, const char* path); Hdf5Handle CreateDatasetSpace(const std::vector& dimensions); +/// @brief Writes quantity identity attributes, without family/frame metadata. +void WriteQuantityAttributes(hid_t dataset, const std::string& component_names, + const std::string& component_units, + const std::string& coordinate_system, + const std::string& location); void WriteResultAttributes(hid_t dataset, const std::string& component_names, const std::string& component_units, const std::string& coordinate_system, diff --git a/src/fesa/io/hdf5/hdf5_result_writer.cpp b/src/fesa/io/hdf5/hdf5_result_writer.cpp index 779b839..d832f9d 100644 --- a/src/fesa/io/hdf5/hdf5_result_writer.cpp +++ b/src/fesa/io/hdf5/hdf5_result_writer.cpp @@ -756,27 +756,19 @@ void WriteResultDatasets(const hid_t file, const Domain& domain, WriteStress(file, state); } -std::vector CopyVector(const Vector& source) { - std::vector values(source.Size()); - for (std::size_t index = 0U; index < source.Size(); ++index) { - values[index] = source[index]; - } - return values; -} - std::vector BuildResultDatasetPlans( const Domain& domain, const AnalysisState& state) { std::vector datasets; const std::vector nodal_dimensions{domain.Nodes().size(), kDofsPerNode}; datasets.push_back({std::string{kStepRoot} + "/nodal/displacement", - nodal_dimensions, CopyVector(state.Displacement()), + nodal_dimensions, CopyVectorValues(state.Displacement()), "UX,UY,UZ,URX,URY,URZ", "length,length,length,radian,radian,radian", "global-cartesian", "nodal"}); datasets.push_back( {std::string{kStepRoot} + "/nodal/reaction", nodal_dimensions, - CopyVector(state.Reaction()), "RF1,RF2,RF3,RM1,RM2,RM3", + CopyVectorValues(state.Reaction()), "RF1,RF2,RF3,RM1,RM2,RM3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "nodal"}); if (IsShellDomain(domain)) { diff --git a/src/fesa/io/hdf5/hdf5_self_check.cpp b/src/fesa/io/hdf5/hdf5_self_check.cpp index 6a77619..2779079 100644 --- a/src/fesa/io/hdf5/hdf5_self_check.cpp +++ b/src/fesa/io/hdf5/hdf5_self_check.cpp @@ -11,6 +11,7 @@ #include "fesa/build_info.h" #include "io/hdf5/hdf5_raii.h" +#include "io/hdf5/hdf5_primitives.h" namespace fesa::hdf5_internal { @@ -596,15 +597,7 @@ void SelfCheckFile(const std::filesystem::path& path, const Domain& domain, std::vector frames; directors.reserve(domain.Nodes().size() * 3U); frames.reserve(domain.Nodes().size() * 9U); - for (const auto& frame : domain.ShellNodeInitialFrames()) { - directors.insert(directors.end(), frame.director.begin(), - frame.director.end()); - frames.insert(frames.end(), frame.tangent_a.begin(), - frame.tangent_a.end()); - frames.insert(frames.end(), frame.tangent_b.begin(), - frame.tangent_b.end()); - frames.insert(frames.end(), frame.director.begin(), frame.director.end()); - } + hdf5_internal::AppendShellReferenceFrames(domain, directors, frames); RequireModelDoubleDataset(file.Get(), "/model/shell/nodal_director", {static_cast(domain.Nodes().size()), 3U}, "D1,D2,D3", "1,1,1", "global-cartesian", "nodal", diff --git a/src/fesa/io/hdf5/nonlinear_hdf5_results_writer.cpp b/src/fesa/io/hdf5/nonlinear_hdf5_results_writer.cpp index dd75d69..cadd201 100644 --- a/src/fesa/io/hdf5/nonlinear_hdf5_results_writer.cpp +++ b/src/fesa/io/hdf5/nonlinear_hdf5_results_writer.cpp @@ -1,6 +1,5 @@ #include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h" -#include #include #include #include @@ -14,17 +13,20 @@ #include "fesa/analysis/analysis_model.h" #include "fesa/build_info.h" -#include "fesa/elements/finite_rotation.h" #include "fesa/fem/dof_manager.h" #include "fesa/math/vector3.h" #include "io/hdf5/hdf5_atomic_file.h" #include "io/hdf5/hdf5_primitives.h" #include "io/hdf5/hdf5_raii.h" #include "io/hdf5/hdf5_self_check.h" +#include "math/finite_values.h" +#include "results/shell_result_recovery.h" namespace fesa { namespace { +using math_internal::IsFinite; + using hdf5_internal::Hdf5Failure; using hdf5_internal::Hdf5Handle; @@ -90,43 +92,9 @@ struct DiagnosticWriteRow { const char* message; }; -template -bool IsFinite(const std::array& values) { - return std::all_of(values.begin(), values.end(), - [](const double value) { return std::isfinite(value); }); -} - -bool IsFinite(const Vector& values) { - for (std::size_t index = 0U; index < values.Size(); ++index) { - if (!std::isfinite(values[index])) { - return false; - } - } - return true; -} - -bool SameIdentity(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; -} - -bool IsFinite(const ShellResultRow& row) { - if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) || - !IsFinite(row.section_resultant)) { - return false; - } - for (const auto& axis : row.local_frame) { - if (!IsFinite(axis)) { - return false; - } - } - for (const auto& stress : row.stress) { - if (!std::isfinite(stress.zeta) || !IsFinite(stress.components)) { - return false; - } - } - return true; +bool SameIdentity(const SourceEntityId& left, + const SourceEntityId& right) { + return fesa::SourceIdentityEquals(left, right); } Status Failure(const char* code, const std::string& message) { @@ -300,7 +268,8 @@ Status ValidateState(const Domain& domain, const NonlinearAnalysisState& state) const std::size_t element = row_index / kShellLocationsPerElement; const std::size_t location = row_index % kShellLocationsPerElement; if (row.element != element || row.location != locations[location] || - row.natural_coordinates != coordinates[location] || !IsFinite(row)) { + row.natural_coordinates != coordinates[location] || + !results_internal::IsFiniteShellRow(row)) { return Failure("invalid-result-state", "Accepted shell rows must preserve element and GP order."); } @@ -315,13 +284,8 @@ void WriteCommonNumericAttributes(const hid_t dataset, const std::string& coordinate_system, const std::string& location) { hdf5_internal::WriteUint64Attribute(dataset, "schema_version", 1U); - hdf5_internal::WriteStringAttribute(dataset, "component_names", - component_names); - hdf5_internal::WriteStringAttribute(dataset, "component_unit_dimensions", - component_units); - hdf5_internal::WriteStringAttribute(dataset, "coordinate_system", - coordinate_system); - hdf5_internal::WriteStringAttribute(dataset, "location", location); + hdf5_internal::WriteQuantityAttributes( + dataset, component_names, component_units, coordinate_system, location); } void WriteFrameAttributes(const hid_t dataset, const AcceptedFrameState& frame) { @@ -660,12 +624,7 @@ void WriteModelData(const hid_t file, const Domain& domain, std::vector frames; directors.reserve(domain.Nodes().size() * 3U); frames.reserve(domain.Nodes().size() * 9U); - for (const auto& frame : domain.ShellNodeInitialFrames()) { - directors.insert(directors.end(), frame.director.begin(), frame.director.end()); - frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end()); - frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end()); - frames.insert(frames.end(), frame.director.begin(), frame.director.end()); - } + hdf5_internal::AppendShellReferenceFrames(domain, directors, frames); const hsize_t node_count = static_cast(domain.Nodes().size()); (void)WriteModelDoubleDataset(file, "/model/shell/nodal_director", {node_count, 3U}, directors.data(), @@ -772,14 +731,6 @@ void WriteAcceptedFrameTable(const hid_t file, hdf5_internal::WriteUint64Attribute(dataset.Get(), "schema_version", 1U); } -std::vector CopyVector(const Vector& source) { - std::vector values(source.Size()); - for (std::size_t index = 0U; index < source.Size(); ++index) { - values[index] = source[index]; - } - return values; -} - std::vector NodalDisplacement(const AcceptedFrameState& frame) { std::vector values(frame.displacement.Size()); for (std::size_t node = 0U; node < frame.nodal_internal_order.size(); ++node) { @@ -793,47 +744,6 @@ std::vector NodalDisplacement(const AcceptedFrameState& frame) { return values; } -std::vector FlattenTriads(const AcceptedFrameState& frame) { - std::vector values; - values.reserve(frame.nodal_triad.size() * 9U); - for (const auto& triad : frame.nodal_triad) { - for (const auto& axis : triad) { - values.insert(values.end(), axis.begin(), axis.end()); - } - } - return values; -} - -std::vector FlattenDirectors(const AcceptedFrameState& frame) { - std::vector values; - values.reserve(frame.nodal_director.size() * 3U); - for (const auto& director : frame.nodal_director) { - values.insert(values.end(), director.begin(), director.end()); - } - return values; -} - -std::vector SpatialReaction(const AcceptedFrameState& frame) { - std::vector values = CopyVector(frame.reaction); - for (std::size_t node = 0U; node < frame.nodal_internal_order.size(); ++node) { - const std::size_t offset = kDofsPerNode * node; - const Vector3 phi{frame.rotation_vector[offset + 3U], - frame.rotation_vector[offset + 4U], - frame.rotation_vector[offset + 5U]}; - const Matrix transform = InverseTransposeLeftJacobianSO3(phi); - const std::array generalized{ - frame.reaction[offset + 3U], frame.reaction[offset + 4U], - frame.reaction[offset + 5U]}; - for (std::size_t row = 0U; row < 3U; ++row) { - values[offset + 3U + row] = 0.0; - for (std::size_t column = 0U; column < 3U; ++column) { - values[offset + 3U + row] += transform(row, column) * generalized[column]; - } - } - } - return values; -} - void WriteShellIdentity(const hid_t dataset, const bool has_section_positions, const std::string& local_frame_path) { hdf5_internal::WriteStringAttribute(dataset, "source_element_type_dataset", @@ -864,35 +774,36 @@ void WriteFrame(const hid_t file, const Domain& domain, file, root + "/nodal/displacement", nodal_dimensions, displacement.data(), displacement.size(), "UX,UY,UZ,URX,URY,URZ", "length,length,length,1,1,1", "global-cartesian", "nodal", frame); - const auto triads = FlattenTriads(frame); + const auto triads = hdf5_internal::FlattenNodalTriads(frame.nodal_triad); (void)WriteFrameDoubleDataset(file, root + "/nodal/triad", {node_count, 3U, 3U}, triads.data(), triads.size(), "X,Y,Z", "1,1,1", "global-cartesian", "nodal-triad", frame); - const auto directors = FlattenDirectors(frame); + const auto directors = hdf5_internal::FlattenNodalDirectors(frame.nodal_director); (void)WriteFrameDoubleDataset(file, root + "/nodal/director", {node_count, 3U}, directors.data(), directors.size(), "D1,D2,D3", "1,1,1", "global-cartesian", "nodal-director", frame); - const auto internal = CopyVector(frame.internal_force); + const auto internal = hdf5_internal::CopyVectorValues(frame.internal_force); (void)WriteFrameDoubleDataset( file, root + "/nodal/generalized_internal_force", nodal_dimensions, internal.data(), internal.size(), "FI1,FI2,FI3,MI_PHI1,MI_PHI2,MI_PHI3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "nodal", frame); - const auto external = CopyVector(frame.external_force); + const auto external = hdf5_internal::CopyVectorValues(frame.external_force); (void)WriteFrameDoubleDataset( file, root + "/nodal/generalized_external_force", nodal_dimensions, external.data(), external.size(), "FE1,FE2,FE3,ME_PHI1,ME_PHI2,ME_PHI3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "nodal", frame); - const auto residual = CopyVector(frame.residual); + const auto residual = hdf5_internal::CopyVectorValues(frame.residual); (void)WriteFrameDoubleDataset( file, root + "/nodal/generalized_residual", nodal_dimensions, residual.data(), residual.size(), "RU1,RU2,RU3,RPHI1,RPHI2,RPHI3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "nodal", frame); - const auto reaction = SpatialReaction(frame); + const auto reaction = hdf5_internal::SpatialReactionValues( + frame.rotation_vector, frame.reaction, frame.nodal_internal_order.size()); (void)WriteFrameDoubleDataset( file, root + "/nodal/reaction", nodal_dimensions, reaction.data(), reaction.size(), "RF1,RF2,RF3,RM1,RM2,RM3", diff --git a/src/fesa/io/hdf5/solid_hdf5_results_writer.cpp b/src/fesa/io/hdf5/solid_hdf5_results_writer.cpp index ba70639..4b72f29 100644 --- a/src/fesa/io/hdf5/solid_hdf5_results_writer.cpp +++ b/src/fesa/io/hdf5/solid_hdf5_results_writer.cpp @@ -27,11 +27,6 @@ using U = std::vector; using B = std::vector; using D = std::vector; using S = std::vector; -std::vector Values(const Vector& vector) { - D result; - for (std::size_t i = 0; i < vector.Size(); ++i) result.push_back(vector[i]); - return result; -} void Parent(hid_t file, const std::string& path) { const auto parent = path.substr(0, path.find_last_of('/')); if (!parent.empty() && H5Lexists(file, parent.c_str(), H5P_DEFAULT) <= 0) @@ -110,11 +105,11 @@ void FrameData(SolidPlan& plan, const Domain& domain, std::size_t frame, value.doubles = {{dynamic ? "time" : "load_factor", coordinate}}; plan.datasets.push_back(std::move(value)); }; - add(Data(root + "/nodal/displacement", {n, 3}, Values(u), "U1,U2,U3", + add(Data(root + "/nodal/displacement", {n, 3}, CopyVectorValues(u), "U1,U2,U3", "length", "nodal")); - add(Data(root + "/nodal/reaction", {n, 3}, Values(rf), "RF1,RF2,RF3", "force", + add(Data(root + "/nodal/reaction", {n, 3}, CopyVectorValues(rf), "RF1,RF2,RF3", "force", "nodal")); - add(Data(root + "/nodal/residual", {n, 3}, Values(r), "R1,R2,R3", "force", + add(Data(root + "/nodal/residual", {n, 3}, CopyVectorValues(r), "R1,R2,R3", "force", "nodal")); D stress, strain; Check(points.size() == 8 * e, "Solid point count is invalid."); @@ -249,7 +244,7 @@ void Model(SolidPlan& plan, const Domain& domain, const DofManager& dofs) { for (std::size_t i = 0; i < domain.Elements().Size(); ++i) { auto element = factory.Create(domain.Elements()[i], domain); Check(element.HasValue(), "Solid factory reference failed."); - const auto* hex = dynamic_cast(element.Value().get()); + auto hex = factory.BindSolidTrial(*element.Value()); Check(hex != nullptr, "Solid kernel identity failed."); auto response = hex->EvaluateLinear(Vector{24}); Check(response.HasValue(), "Solid reference recovery failed."); @@ -426,15 +421,15 @@ Result BuildSolidPlan(const Domain& domain, SolidStateView view, plan.datasets.push_back(std::move(data)); }; add(Data(root + "/nodal/velocity", {domain.Nodes().size(), 3}, - Values(frame.velocity), "V1,V2,V3", "length/time", "nodal")); + CopyVectorValues(frame.velocity), "V1,V2,V3", "length/time", "nodal")); auto a = Data(root + "/nodal/acceleration", {domain.Nodes().size(), 3}, - Values(frame.acceleration), "A1,A2,A3", "length/time^2", + CopyVectorValues(frame.acceleration), "A1,A2,A3", "length/time^2", "nodal"); a.strings["acceleration_kind"] = "physical"; add(std::move(a)); a = Data(root + "/nodal/algorithmic_acceleration", {domain.Nodes().size(), 3}, - Values(frame.algorithmic_acceleration), "A1,A2,A3", + CopyVectorValues(frame.algorithmic_acceleration), "A1,A2,A3", "length/time^2", "nodal"); a.strings["acceleration_kind"] = "algorithmic"; add(std::move(a)); diff --git a/src/fesa/io/hdf5/thermal_hdf5_self_check.cpp b/src/fesa/io/hdf5/thermal_hdf5_self_check.cpp index fb19698..bf2ddfb 100644 --- a/src/fesa/io/hdf5/thermal_hdf5_self_check.cpp +++ b/src/fesa/io/hdf5/thermal_hdf5_self_check.cpp @@ -7,7 +7,9 @@ #include #include +#include "fesa/analysis/analysis_model.h" #include "fesa/build_info.h" +#include "fesa/fem/dof_manager.h" #include "io/hdf5/hdf5_primitives.h" #include "io/hdf5/hdf5_raii.h" @@ -424,8 +426,7 @@ void Frames(hid_t f, const ThermalAnalysisState& s) { Number(g.Get(), "is_initial", H5T_STD_U8LE, H5T_NATIVE_UINT8, static_cast(frame.is_initial)); ResultValues(f, root + "/nodal/temperature", {s.NodeOrder().size(), 5}, - {frame.temperature.Data(), - frame.temperature.Data() + frame.temperature.Size()}, + CopyVectorValues(frame.temperature), "NT11,NT12,NT13,NT14,NT15", "temperature", "scalar", "nodal-thickness-stations"); if (frame.is_initial) { @@ -433,9 +434,7 @@ void Frames(hid_t f, const ThermalAnalysisState& s) { Absent(f, root + "/element", "heat_flux"); } else { const auto& payload = *frame.completed; - std::vector reaction(payload.reaction_heat_rate.Size()); - for (std::size_t i = 0; i < reaction.size(); ++i) - reaction[i] = payload.reaction_heat_rate[i]; + const auto reaction = CopyVectorValues(payload.reaction_heat_rate); ResultValues(f, root + "/nodal/reaction_heat_rate", {reaction.size()}, reaction, "RFL", "power", "scalar", "prescribed-temperature-dofs"); diff --git a/src/fesa/loads/shell_surface_pressure.cpp b/src/fesa/loads/shell_surface_pressure.cpp index 603fda5..01a9896 100644 --- a/src/fesa/loads/shell_surface_pressure.cpp +++ b/src/fesa/loads/shell_surface_pressure.cpp @@ -6,10 +6,13 @@ #include #include #include +#include "math/vector3_operations.h" namespace fesa { namespace { +using math_internal::WeightedSum; + constexpr std::size_t kNodeCount = 4U; constexpr std::size_t kDofsPerNode = 6U; constexpr std::size_t kDofCount = kNodeCount * kDofsPerNode; @@ -54,16 +57,6 @@ ShapeData ShapeAt(const double xi, const double eta) noexcept { 0.25 * (1.0 + xi), 0.25 * (1.0 - xi)}}}; } -/// @brief Evaluates one tangent or area vector from shape coefficients. -Vector3 WeightedSum(const std::array& weights, - const std::array& coordinates) { - Vector3 result{}; - for (std::size_t node = 0U; node < kNodeCount; ++node) { - result = result + weights[node] * coordinates[node]; - } - return result; -} - /// @brief Evaluates finite bilinear surface data at one natural point. bool EvaluateGeometry(const std::array& coordinates, const double xi, const double eta, diff --git a/src/fesa/math/compensated_sum.h b/src/fesa/math/compensated_sum.h new file mode 100644 index 0000000..7201513 --- /dev/null +++ b/src/fesa/math/compensated_sum.h @@ -0,0 +1,30 @@ +#ifndef FESA_MATH_COMPENSATED_SUM_H_ +#define FESA_MATH_COMPENSATED_SUM_H_ + +#include + +namespace fesa::math_internal { + +/// @brief Preserves the compensated scalar accumulation graph and caller order. +inline void AddCompensated(const double term, double* sum, double* correction) { + const double next = *sum + term; + *correction += std::abs(*sum) >= std::abs(term) + ? (*sum - next) + term + : (term - next) + *sum; + *sum = next; +} + +/// @brief Owns one sum and its correction without changing reduction order. +class CompensatedSum { + public: + void Add(const double term) { AddCompensated(term, &sum_, &correction_); } + double Value() const { return sum_ + correction_; } + + private: + double sum_ = 0.0; + double correction_ = 0.0; +}; + +} // namespace fesa::math_internal + +#endif // FESA_MATH_COMPENSATED_SUM_H_ diff --git a/src/fesa/math/dense_blas_internal.cpp b/src/fesa/math/dense_blas_internal.cpp index ed5de9d..bc03aba 100644 --- a/src/fesa/math/dense_blas_internal.cpp +++ b/src/fesa/math/dense_blas_internal.cpp @@ -7,8 +7,271 @@ #include #include +#include "math/finite_values.h" + namespace fesa::dense_blas_internal { +namespace { + +using math_internal::IsFinite; + +/// @brief Keeps dense failures backend-neutral for caller-specific diagnostics. +Status FactorFailure(const char* code, const char* message) { + return Status::Failure({{Severity::kError, code, {}, "", "", message}}); +} + +/// @brief Preserves each value owner's size exception at the private boundary. +MKL_INT MklSizeOrThrow(const std::size_t size, const char* message) { + const auto converted = ToMklSize(size); + if (!converted.HasValue()) throw std::length_error{message}; + return converted.Value(); +} + +/// @brief Converts a contiguous BLAS vector length with its existing contract. +MKL_INT VectorSizeOrThrow(const std::size_t size) { + return MklSizeOrThrow(size, + "Dense vector size exceeds the MKL integer range."); +} + +/// @brief Converts a matrix dimension with its existing exception contract. +MKL_INT MatrixSizeOrThrow(const std::size_t size) { + return MklSizeOrThrow(size, + "Dense matrix dimension exceeds the MKL integer range."); +} + +/// @brief Rejects Matrix product inputs before allocation or empty returns. +void RequireFinite(const double* values, const std::size_t size) { + for (std::size_t i = 0; i < size; ++i) + if (!std::isfinite(values[i])) + throw std::invalid_argument{"Matrix product inputs must be finite."}; +} + +} // namespace + +DenseFactor::DenseFactor(const Matrix& coefficients, + const bool positive_definite) + : factors_{coefficients}, + pivots_(positive_definite ? 0U : coefficients.Rows()), + positive_definite_{positive_definite} { + for (std::size_t row = 0; row < coefficients.Rows(); ++row) { + double row_sum = 0.0; + for (std::size_t column = 0; column < coefficients.Columns(); ++column) + row_sum += std::abs(coefficients(row, column)); + coefficient_norm_inf_ = (std::max)(coefficient_norm_inf_, row_sum); + } +} + +Result DenseFactor::Lu(const Matrix& coefficients) { + return Create(coefficients, false); +} + +Result DenseFactor::Cholesky(const Matrix& coefficients) { + return Create(coefficients, true); +} + +Result DenseFactor::Create(const Matrix& coefficients, + const bool positive_definite) { + if (coefficients.Rows() != coefficients.Columns()) + return Result::Failure(FactorFailure( + "dense-factor-shape", "Dense factor requires a square matrix.")); + const auto size = ToMklSize(coefficients.Rows()); + if (!size.HasValue()) return Result::Failure(size.GetStatus()); + if (!IsFinite(coefficients)) + return Result::Failure(FactorFailure( + "dense-factor-nonfinite", "Dense factor inputs must be finite.")); + DenseFactor candidate{coefficients, positive_definite}; + if (size.Value() != 0) { + const auto info = + positive_definite + ? LAPACKE_dpotrf(LAPACK_ROW_MAJOR, 'L', size.Value(), + candidate.factors_.Data(), size.Value()) + : LAPACKE_dgetrf(LAPACK_ROW_MAJOR, size.Value(), size.Value(), + candidate.factors_.Data(), size.Value(), + candidate.pivots_.data()); + if (info != 0 || !IsFinite(candidate.factors_)) + return Result::Failure(FactorFailure( + "dense-factor-pivot", "Dense numerical factorization failed.")); + } + return Result::Success(std::move(candidate)); +} + +Result DenseFactor::Solve(const Matrix& rhs, + const bool transpose) const { + return SolveImpl(rhs, transpose, false, false); +} + +Result DenseFactor::SolveRight(const Matrix& rhs, + const bool transpose) const { + return SolveImpl(rhs, transpose, true, false); +} + +Result DenseFactor::SolveLower(const Matrix& rhs, + const bool transpose) const { + return SolveImpl(rhs, transpose, false, true); +} + +Result DenseFactor::SolveLowerRight(const Matrix& rhs, + const bool transpose) const { + return SolveImpl(rhs, transpose, true, true); +} + +Result DenseFactor::SolveImpl(const Matrix& rhs, const bool transpose, + const bool right, + const bool lower_only) const { + const auto order = factors_.Rows(); + if ((right ? rhs.Columns() : rhs.Rows()) != order || + (lower_only && !positive_definite_)) + return Result::Failure(FactorFailure( + "dense-solve-shape", "Dense solve shape or factor property differs.")); + const auto count = ToMklSize(right ? rhs.Rows() : rhs.Columns()); + if (!count.HasValue()) return Result::Failure(count.GetStatus()); + if (!IsFinite(rhs)) + return Result::Failure(FactorFailure( + "dense-solve-nonfinite", "Dense solve RHS must be finite.")); + if (order == 0U || count.Value() == 0) + return Result::Success(Matrix{rhs}); + Matrix candidate = right && !lower_only ? Matrix{order, rhs.Rows()} + : Matrix{rhs}; + const auto size = ToMklSize(order).Value(); + if (lower_only) { + cblas_dtrsm(CblasRowMajor, right ? CblasRight : CblasLeft, CblasLower, + transpose ? CblasTrans : CblasNoTrans, CblasNonUnit, + right ? count.Value() : size, right ? size : count.Value(), 1.0, + factors_.Data(), size, candidate.Data(), + right ? size : count.Value()); + } else { + // X*op(A)=B becomes op(A)^T*X^T=B^T. GETRS uses a compact n-by-nrhs + // row-major RHS with ldb=nrhs, including nonsquare multi-RHS storage. + if (right) { + for (std::size_t i = 0; i < rhs.Rows(); ++i) + for (std::size_t j = 0; j < order; ++j) candidate(j, i) = rhs(i, j); + } + const auto info = + positive_definite_ + ? LAPACKE_dpotrs(LAPACK_ROW_MAJOR, 'L', size, count.Value(), + factors_.Data(), size, candidate.Data(), + count.Value()) + : LAPACKE_dgetrs(LAPACK_ROW_MAJOR, transpose != right ? 'T' : 'N', + size, count.Value(), factors_.Data(), size, + pivots_.data(), candidate.Data(), count.Value()); + if (info != 0) + return Result::Failure( + FactorFailure("dense-solve-backend", "Dense backend solve failed.")); + if (right) { + Matrix transposed{rhs.Rows(), order}; + for (std::size_t i = 0; i < rhs.Rows(); ++i) + for (std::size_t j = 0; j < order; ++j) + transposed(i, j) = candidate(j, i); + candidate = std::move(transposed); + } + } + if (!IsFinite(candidate)) + return Result::Failure(FactorFailure( + "dense-solve-nonfinite", "Dense solve result is nonfinite.")); + return Result::Success(std::move(candidate)); +} + +Result DenseFactor::Lower() const { + if (!positive_definite_) + return Result::Failure(FactorFailure( + "dense-factor-property", "An LU factor has no Cholesky lower factor.")); + Matrix lower{factors_.Rows(), factors_.Columns()}; + for (std::size_t i = 0; i < factors_.Rows(); ++i) + for (std::size_t j = 0; j <= i; ++j) lower(i, j) = factors_(i, j); + return Result::Success(std::move(lower)); +} + +double DenseFactor::Determinant() const { + double determinant = 1.0; + for (std::size_t i = 0; i < factors_.Rows(); ++i) { + determinant *= factors_(i, i); + if (positive_definite_) + determinant *= factors_(i, i); + else if (pivots_[i] != static_cast(i + 1U)) + determinant = -determinant; + } + return determinant; +} + +Result DenseFactor::ReciprocalConditionInfinity() const { + if (!(coefficient_norm_inf_ > 0.0) || !std::isfinite(coefficient_norm_inf_)) + return Result::Failure(FactorFailure( + "dense-condition-norm", "Dense coefficient infinity norm is invalid.")); + const auto order = factors_.Rows(); + std::vector row_sums(order, 0.0); + Matrix rhs{order, 1U}; + for (std::size_t column = 0; column < order; ++column) { + if (column != 0U) rhs(column - 1U, 0U) = 0.0; + rhs(column, 0U) = 1.0; + const auto solved = Solve(rhs); + if (!solved.HasValue()) return Result::Failure(solved.GetStatus()); + for (std::size_t row = 0; row < order; ++row) + row_sums[row] += std::abs(solved.Value()(row, 0U)); + } + double inverse_norm = 0.0; + for (const auto row_sum : row_sums) + inverse_norm = (std::max)(inverse_norm, row_sum); + if (!(inverse_norm > 0.0) || !std::isfinite(inverse_norm)) + return Result::Failure(FactorFailure( + "dense-condition-norm", "Dense inverse infinity norm is invalid.")); + return Result::Success((1.0 / coefficient_norm_inf_) / inverse_norm); +} + +double DotValues(const double* left, const std::size_t size, + const double* right) { + if (size == 0U) return 0.0; + return cblas_ddot(VectorSizeOrThrow(size), left, 1, right, 1); +} + +double NormValues(const double* values, const std::size_t size) { + if (size == 0U) return 0.0; + return cblas_dnrm2(VectorSizeOrThrow(size), values, 1); +} + +void ScaleValues(const double alpha, double* values, const std::size_t size) { + if (size == 0U) return; + cblas_dscal(VectorSizeOrThrow(size), alpha, values, 1); +} + +void AxpyValues(const double alpha, const double* x, const std::size_t size, + double* y) { + if (size == 0U) return; + cblas_daxpy(VectorSizeOrThrow(size), alpha, x, 1, y, 1); +} + +Vector Multiply(const Matrix& left, const Vector& right) { + if (left.Columns() != right.Size()) + throw std::invalid_argument{ + "Matrix-vector multiplication has incompatible dimensions."}; + RequireFinite(left.Data(), left.Rows() * left.Columns()); + RequireFinite(right.Data(), right.Size()); + Vector result{left.Rows()}; + if (left.Rows() == 0U || left.Columns() == 0U) return result; + const auto rows = MatrixSizeOrThrow(left.Rows()); + const auto columns = MatrixSizeOrThrow(left.Columns()); + cblas_dgemv(CblasRowMajor, CblasNoTrans, rows, columns, 1.0, left.Data(), + columns, right.Data(), 1, 0.0, result.Data(), 1); + return result; +} + +Matrix Multiply(const Matrix& left, const Matrix& right) { + if (left.Columns() != right.Rows()) + throw std::invalid_argument{ + "Matrix multiplication has incompatible dimensions."}; + RequireFinite(left.Data(), left.Rows() * left.Columns()); + RequireFinite(right.Data(), right.Rows() * right.Columns()); + Matrix result{left.Rows(), right.Columns()}; + if (left.Rows() == 0U || left.Columns() == 0U || right.Columns() == 0U) + return result; + const auto rows = MatrixSizeOrThrow(left.Rows()); + const auto columns = MatrixSizeOrThrow(right.Columns()); + const auto inner = MatrixSizeOrThrow(left.Columns()); + cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, rows, columns, inner, + 1.0, left.Data(), inner, right.Data(), columns, 0.0, + result.Data(), columns); + return result; +} + ScopedSingleEquationThreads::ScopedSingleEquationThreads( const std::size_t equation_count) noexcept { // The measured 1x1 PARDISO case has backend/thread overhead and an @@ -45,60 +308,6 @@ LocalDensePolicy ReadScopedLocalDensePolicy() noexcept { 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; - if (matrix.Rows() != matrix.Columns()) - throw std::invalid_argument{"Cholesky requires a square matrix."}; - const auto size = ToMklSize(matrix.Rows()); - if (!size.HasValue()) - throw std::length_error{"Cholesky size exceeds the MKL integer range."}; - Matrix candidate_lower{matrix.Rows(), matrix.Columns()}; - Matrix candidate_inverse{matrix.Rows(), matrix.Columns()}; - if (matrix.Rows() == 0U) { - lower = std::move(candidate_lower); - inverse_lower = std::move(candidate_inverse); - return true; - } - std::vector values(matrix.Rows() * matrix.Columns()); - for (std::size_t row = 0; row < matrix.Rows(); ++row) - for (std::size_t column = 0; column < matrix.Columns(); ++column) { - if (!std::isfinite(matrix(row, column))) - throw std::invalid_argument{"Cholesky inputs must be finite."}; - values[row * matrix.Columns() + column] = matrix(row, column); - } - if (LAPACKE_dpotrf(LAPACK_ROW_MAJOR, 'L', size.Value(), values.data(), - size.Value()) != 0) { - if (failure != nullptr) *failure = CholeskyFailure::kPivot; - return false; - } - for (std::size_t row = 0; row < matrix.Rows(); ++row) - for (std::size_t column = 0; column <= row; ++column) { - candidate_lower(row, column) = values[row * matrix.Columns() + column]; - if (!std::isfinite(candidate_lower(row, column))) { - if (failure != nullptr) *failure = CholeskyFailure::kPivot; - return false; - } - } - if (LAPACKE_dtrtri(LAPACK_ROW_MAJOR, 'L', 'N', size.Value(), values.data(), - size.Value()) != 0) { - if (failure != nullptr) *failure = CholeskyFailure::kInverse; - return false; - } - for (std::size_t row = 0; row < matrix.Rows(); ++row) - for (std::size_t column = 0; column <= row; ++column) { - const auto value = values[row * matrix.Columns() + column]; - if (!std::isfinite(value)) { - if (failure != nullptr) *failure = CholeskyFailure::kInverse; - return false; - } - candidate_inverse(row, column) = value; - } - lower = std::move(candidate_lower); - inverse_lower = std::move(candidate_inverse); - return true; -} - Vector MultiplyDiagonal(const Vector& diagonal, const Vector& rhs) { if (diagonal.Size() != rhs.Size()) throw std::invalid_argument{"Diagonal product dimensions differ."}; @@ -122,11 +331,19 @@ Matrix MultiplyTransposeLeft(const Matrix& left, const Matrix& right) { if (left.Rows() != right.Rows()) { throw std::invalid_argument{"Transpose product dimensions differ."}; } - Matrix transposed{left.Columns(), left.Rows()}; - for (std::size_t row = 0; row < left.Rows(); ++row) - for (std::size_t column = 0; column < left.Columns(); ++column) - transposed(column, row) = left(row, column); - return transposed.Multiply(right); + RequireFinite(left.Data(), left.Rows() * left.Columns()); + RequireFinite(right.Data(), right.Rows() * right.Columns()); + Matrix result{left.Columns(), right.Columns()}; + if (left.Columns() == 0U || left.Rows() == 0U || right.Columns() == 0U) + return result; + const auto rows = MatrixSizeOrThrow(left.Columns()); + const auto columns = MatrixSizeOrThrow(right.Columns()); + const auto inner = MatrixSizeOrThrow(left.Rows()); + // Row-major A is stored inner-by-rows even when GEMM consumes A-transpose. + cblas_dgemm(CblasRowMajor, CblasTrans, CblasNoTrans, rows, columns, inner, + 1.0, left.Data(), rows, right.Data(), columns, 0.0, + result.Data(), columns); + return result; } bool OuterProduct(const double* left, const std::size_t rows, diff --git a/src/fesa/math/dense_blas_internal.h b/src/fesa/math/dense_blas_internal.h index b2d4481..037c94a 100644 --- a/src/fesa/math/dense_blas_internal.h +++ b/src/fesa/math/dense_blas_internal.h @@ -2,6 +2,7 @@ #define FESA_SRC_FESA_MATH_DENSE_BLAS_INTERNAL_H_ #include +#include #include @@ -11,6 +12,73 @@ namespace fesa::dense_blas_internal { +/// @brief Owns a double-precision numerical factor of a coefficient snapshot. +/// @note Repeated RHS may reuse this immutable factor concurrently. Changing +/// coefficients, dimension, matrix property or precision requires a new factor; +/// matching sparsity alone never permits numerical reuse. No symbolic cache or +/// borrowed coefficient storage exists. Backend thread policy stays +/// caller-owned. +class DenseFactor { + public: + /// @brief Factors a finite square general matrix with partial-pivoting LU. + /// @return An independent factor or a shape, finite, or singular failure. + static Result Lu(const Matrix& coefficients); + + /// @brief Factors the lower triangle of a positive definite matrix. + /// @pre The caller establishes symmetry, including its own tolerance policy. + /// @return An independent factor or a shape, finite, or pivot failure. + /// @note All entries are checked for finiteness; upper values are not + /// averaged. + static Result Cholesky(const Matrix& coefficients); + + /// @brief Solves op(A)*X=rhs with one or more row-major RHS columns. + /// @return A finite owned solution or failure, leaving factor and rhs intact. + Result Solve(const Matrix& rhs, bool transpose = false) const; + + /// @brief Solves X*op(A)=rhs with one or more row-major RHS rows. + /// @return A finite owned solution or failure, leaving factor and rhs intact. + Result SolveRight(const Matrix& rhs, bool transpose = false) const; + + /// @brief Solves op(L)*X=rhs using this Cholesky lower factor. + /// @return Failure for an LU factor, incompatible shape or nonfinite result. + Result SolveLower(const Matrix& rhs, bool transpose = false) const; + + /// @brief Solves X*op(L)=rhs using this Cholesky lower factor. + /// @return Failure for an LU factor, incompatible shape or nonfinite result. + Result SolveLowerRight(const Matrix& rhs, + bool transpose = false) const; + + /// @brief Returns an owned lower Cholesky factor with an exact zero upper + /// part. + /// @return Failure for an LU factor. + Result Lower() const; + + /// @brief Computes the signed determinant from pivots and diagonal factors. + /// @note Empty determinant is one; double overflow/underflow propagates. + double Determinant() const; + + /// @brief Computes (1/||A||inf)/||A^-1||inf without retaining an inverse. + /// @note Solves canonical RHS in ascending order and streams row absolute + /// sums. No estimator, acceptance threshold or regularization is applied. + /// @return Failure for an empty matrix or nonfinite norm/solution arithmetic. + Result ReciprocalConditionInfinity() const; + + private: + /// @brief Constructs a candidate before any backend factorization succeeds. + DenseFactor(const Matrix& coefficients, bool positive_definite); + /// @brief Implements factor creation while keeping failed candidates local. + static Result Create(const Matrix& coefficients, + bool positive_definite); + /// @brief Solves into a candidate with a shared finite/shape failure gate. + Result SolveImpl(const Matrix& rhs, bool transpose, bool right, + bool lower_only) const; + + Matrix factors_; + std::vector pivots_; + double coefficient_norm_inf_{0.0}; + bool positive_definite_; +}; + /// @brief Limits only default-policy one-equation backend calls to one thread. /// @note Existing caller TLS overrides are retained. Destruction restores the /// default request; process-wide MKL/OpenMP settings are never changed. @@ -54,6 +122,37 @@ LocalDensePolicy ReadScopedLocalDensePolicy() noexcept; /// @return The converted length or an uncategorized overflow failure. Result ToMklSize(std::size_t size); +/// @brief Computes a contiguous dot product without rejecting nonfinite input. +/// @pre Both arrays contain size readable entries; null is valid for size zero. +/// @throws std::length_error if size exceeds the backend integer range. +double DotValues(const double* left, std::size_t size, const double* right); + +/// @brief Computes a robust contiguous Euclidean norm with BLAS propagation. +/// @pre values contains size readable entries; null is valid for size zero. +/// @throws std::length_error if size exceeds the backend integer range. +double NormValues(const double* values, std::size_t size); + +/// @brief Scales contiguous values with BLAS nonfinite propagation. +/// @pre values contains size writable entries; null is valid for size zero. +/// @throws std::length_error if size exceeds the backend integer range. +void ScaleValues(double alpha, double* values, std::size_t size); + +/// @brief Accumulates alpha*x into y, allowing x and y to be identical. +/// @pre Arrays contain size entries; other overlap is unsupported. +/// @note Null storage is valid for size zero; nonfinite values follow BLAS. +/// @throws std::length_error if size exceeds the backend integer range. +void AxpyValues(double alpha, const double* x, std::size_t size, double* y); + +/// @brief Multiplies owned row-major values with the Matrix failure contract. +/// @throws std::invalid_argument for incompatible shapes or nonfinite input. +/// @throws std::length_error if dimensions exceed the backend integer range. +Vector Multiply(const Matrix& left, const Vector& right); + +/// @brief Multiplies owned row-major matrices with the Matrix failure contract. +/// @throws std::invalid_argument for incompatible shapes or nonfinite input. +/// @throws std::length_error if dimensions exceed the backend integer range. +Matrix Multiply(const Matrix& left, const Matrix& right); + /// @brief Computes left-transpose times right through ordinary dense MKL. /// @throws std::invalid_argument for mismatched rows or nonfinite operands. Matrix MultiplyTransposeLeft(const Matrix& left, const Matrix& right); @@ -83,19 +182,6 @@ bool MultiplyTransposeRight(const double* left, std::size_t rows, const double* right, std::size_t columns, std::size_t inner, double* destination); -/// @brief Distinguishes positive-factor failure from inverse failure. -enum class CholeskyFailure { kNone, kPivot, kInverse }; - -/// @brief Computes a lower Cholesky factor and its inverse with MKL LAPACK. -/// @return False for a nonpositive pivot or nonfinite factor/inverse. -/// @note Outputs are replaced only after both finite candidates succeed. -/// The optional failure output is initialized even when validation throws. -/// @throws std::invalid_argument for a nonsquare or nonfinite input. -/// @throws std::length_error for a backend size overflow. -bool CholeskyLowerAndInverse(const Matrix& matrix, Matrix& lower, - Matrix& inverse_lower, - CholeskyFailure* failure = nullptr); - /// @brief Copies contiguous double values through the private BLAS backend. /// @param source Readable storage for size values when size is nonzero. /// @param size Number of contiguous values to copy. diff --git a/src/fesa/math/finite_values.h b/src/fesa/math/finite_values.h new file mode 100644 index 0000000..3433915 --- /dev/null +++ b/src/fesa/math/finite_values.h @@ -0,0 +1,40 @@ +#ifndef FESA_MATH_FINITE_VALUES_H_ +#define FESA_MATH_FINITE_VALUES_H_ + +#include +#include +#include + +#include "fesa/math/matrix.h" +#include "fesa/math/vector.h" + +namespace fesa::math_internal { + +/// @brief Checks ordinary doubles in ascending order without dereferencing empty storage. +inline bool IsFiniteValues(const double* values, const std::size_t size) { + for (std::size_t index = 0U; index < size; ++index) { + if (!std::isfinite(values[index])) { + return false; + } + } + return true; +} + +template +bool IsFinite(const std::array& values) { + return IsFiniteValues(values.data(), Size); +} + +inline bool IsFinite(const Vector& values) { + return IsFiniteValues(values.Data(), values.Size()); +} + +inline bool IsFinite(const Matrix& values) { + const auto size = values.Rows() * values.Columns(); + const double* const data = values.Data(); + return IsFiniteValues(data, size); +} + +} // namespace fesa::math_internal + +#endif // FESA_MATH_FINITE_VALUES_H_ diff --git a/src/fesa/math/high_low_arithmetic.h b/src/fesa/math/high_low_arithmetic.h index 8e9bde6..9776724 100644 --- a/src/fesa/math/high_low_arithmetic.h +++ b/src/fesa/math/high_low_arithmetic.h @@ -88,6 +88,12 @@ inline double Rounded(HighLow value) { /// @brief Adds normalized expansions; rounding of discarded tail is bounded. inline HighLow Add(HighLow left, HighLow right) { if (!IsNormalized(left) || !IsNormalized(right)) return InvalidHighLow(); + // Exact-zero additions only normalize signed zero in the retained pair. + // The nonzero graph and its discarded-tail association stay unchanged. + if (left.high == 0 && left.low == 0) + return {right.high, right.low == 0 ? 0.0 : right.low}; + if (right.high == 0 && right.low == 0) + return {left.high, left.low == 0 ? 0.0 : left.low}; const auto head = TwoSum(left.high, right.high); const auto tail = TwoSum(left.low, right.low); const auto middle = TwoSum(head.low, tail.high); @@ -164,9 +170,16 @@ inline HighLow TwoProduct(double left, double right) { inline HighLow Multiply(HighLow left, HighLow right) { if (!IsNormalized(left) || !IsNormalized(right)) return InvalidHighLow(); auto product = TwoProduct(left.high, right.high); - product = Add(product, TwoProduct(left.high, right.low)); - product = Add(product, TwoProduct(left.low, right.high)); - return Add(product, TwoProduct(left.low, right.low)); + // Omitted terms are exact zero; retain the zero normalization that preceded + // a nonzero low-high term and the final canonicalization/invalid check. + if (right.low != 0) + product = Add(product, TwoProduct(left.high, right.low)); + else if (left.low != 0) + product = Add(product, HighLow{}); + if (left.low != 0) + product = Add(product, TwoProduct(left.low, right.high)); + return Add(product, left.low != 0 && right.low != 0 + ? TwoProduct(left.low, right.low) : HighLow{}); } /// @brief Orders canonical values; 2 means unordered/unsupported input. inline int Compare(HighLow left, HighLow right) { diff --git a/src/fesa/math/jet_arithmetic.h b/src/fesa/math/jet_arithmetic.h new file mode 100644 index 0000000..439e439 --- /dev/null +++ b/src/fesa/math/jet_arithmetic.h @@ -0,0 +1,38 @@ +#ifndef FESA_SRC_FESA_MATH_JET_ARITHMETIC_H_ +#define FESA_SRC_FESA_MATH_JET_ARITHMETIC_H_ + +#include +#include + +namespace fesa::math_internal { +/// @brief Multiplies fixed Jet matrices in row/column/index order. +/// @note The caller retains its Jet precision, derivative storage, product rule +/// and constant provenance. Each result starts at its actual constant zero. +template +std::array, 3> MultiplyJetMatrix( + const std::array, 3>& left, + const std::array, 3>& right, Constant constant) { + std::array, 3> result{}; + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t column = 0; column < 3; ++column) { + result[row][column] = constant(0.0); + for (std::size_t index = 0; index < 3; ++index) + result[row][column] = result[row][column] + + left[row][index] * right[index][column]; + } + return result; +} +/// @brief Multiplies a fixed Jet matrix and actual vector RHS in row order. +/// @note The zero-start component accumulation matches both Jet consumers. +template +std::array MultiplyJetMatrixVector( + const std::array, 3>& matrix, + const std::array& vector, Constant constant) { + std::array result{constant(0.0),constant(0.0),constant(0.0)}; + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t column = 0; column < 3; ++column) + result[row] = result[row] + matrix[row][column] * vector[column]; + return result; +} +} // namespace fesa::math_internal +#endif // FESA_SRC_FESA_MATH_JET_ARITHMETIC_H_ diff --git a/src/fesa/math/jet_linear_solve.h b/src/fesa/math/jet_linear_solve.h new file mode 100644 index 0000000..8bb63b1 --- /dev/null +++ b/src/fesa/math/jet_linear_solve.h @@ -0,0 +1,85 @@ +#ifndef FESA_SRC_FESA_MATH_JET_LINEAR_SOLVE_H_ +#define FESA_SRC_FESA_MATH_JET_LINEAR_SOLVE_H_ + +#include +#include +#include + +#include "dense_blas_internal.h" + +namespace fesa::math_internal { +/// @brief Solves one three-component Jet action by increasing derivative degree. +/// @pre factor is LU of exactly the value coefficients of a. Channel zero of +/// degree zero is the value; each later degree contains every retained channel. +/// The reader/writer preserve the caller's derivative or Taylor convention. +/// @note Each degree solves A0*x_degree=(b-A*x_lower)_degree using the same +/// immutable factor. The caller's product supplies every product-rule term; +/// the not-yet-written degree is zero. Distinct Jet storage/order stays owned +/// by the caller. No inverse or primal-only approximation is formed. +/// @return False on a nonfinite solve, leaving output and inputs unchanged. +template +bool SolveJetAction(const std::array, 3>& a, + const std::array& b, + const dense_blas_internal::DenseFactor& factor, + const std::array& channels, + Constant constant, Read read, Write write, + std::array& output) { + std::array candidate{constant(0.0), constant(0.0), constant(0.0)}; + for (std::size_t degree = 0; degree < channels.size(); ++degree) { + Matrix rhs{3, channels[degree]}; + for (std::size_t row = 0; row < 3; ++row) { + Jet residual = b[row]; + // At degree zero the entire candidate is zero. Avoid 0*nonfinite higher + // derivatives affecting a value-only RHS before their own finite gate. + if (degree != 0) + for (std::size_t column = 0; column < 3; ++column) + residual = residual - a[row][column] * candidate[column]; + for (std::size_t channel = 0; channel < channels[degree]; ++channel) + rhs(row, channel) = read(residual, degree, channel); + } + const auto solved = factor.Solve(rhs); + if (!solved.HasValue()) return false; + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t channel = 0; channel < channels[degree]; ++channel) + write(candidate[row], degree, channel, solved.Value()(row, channel)); + } + output = std::move(candidate); + return true; +} +/// @brief Applies a fixed double factor to every channel of one Jet RHS. +/// @note Fixed coefficients have no variations, so the differentiated equation +/// is A0*x_channel=b_channel with no lower-order product terms. Packing never +/// rounds, contracts or omits a derivative channel. Candidate publication is +/// atomic and the factor remains immutable. +template +bool SolveConstantJetAction(const std::array& b, + const dense_blas_internal::DenseFactor& factor, + bool transpose, + const std::array& channels, + Constant constant, Read read, Write write, + std::array& output) { + std::size_t count = 0; + for (const auto size : channels) count += size; + Matrix rhs{3, count}; + std::size_t offset = 0; + for (std::size_t degree = 0; degree < channels.size(); ++degree) { + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t channel = 0; channel < channels[degree]; ++channel) + rhs(row, offset + channel) = read(b[row], degree, channel); + offset += channels[degree]; + } + const auto solved = factor.Solve(rhs, transpose); + if (!solved.HasValue()) return false; + std::array candidate{constant(0), constant(0), constant(0)}; + offset = 0; + for (std::size_t degree = 0; degree < channels.size(); ++degree) { + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t channel = 0; channel < channels[degree]; ++channel) + write(candidate[row], degree, channel, solved.Value()(row, offset+channel)); + offset += channels[degree]; + } + output = std::move(candidate); + return true; +} +} // namespace fesa::math_internal +#endif // FESA_SRC_FESA_MATH_JET_LINEAR_SOLVE_H_ diff --git a/src/fesa/math/matrix.cpp b/src/fesa/math/matrix.cpp index a28e167..b991f24 100644 --- a/src/fesa/math/matrix.cpp +++ b/src/fesa/math/matrix.cpp @@ -1,8 +1,5 @@ #include "fesa/math/matrix.h" -#include -#include - #include #include #include @@ -24,17 +21,6 @@ std::size_t CheckedStorageSize(const std::size_t rows, return rows * columns; } -/// @brief Preserves the Matrix exception contract around private size -/// conversion. -MKL_INT MklSizeOrThrow(const std::size_t size) { - const auto converted = dense_blas_internal::ToMklSize(size); - if (!converted.HasValue()) { - throw std::length_error{ - "Dense matrix dimension exceeds the MKL integer range."}; - } - return converted.Value(); -} - } // namespace Matrix::Matrix(const std::size_t rows, const std::size_t columns, @@ -88,6 +74,18 @@ std::size_t Matrix::Rows() const noexcept { return rows_; } std::size_t Matrix::Columns() const noexcept { return columns_; } +double* Matrix::Data() noexcept { return values_.data(); } + +const double* Matrix::Data() const noexcept { return values_.data(); } + +double Matrix::Norm() const { + return dense_blas_internal::NormValues(Data(), values_.size()); +} + +void Matrix::Scale(const double alpha) { + dense_blas_internal::ScaleValues(alpha, Data(), values_.size()); +} + double& Matrix::operator()(const std::size_t row, const std::size_t column) { if (row >= rows_ || column >= columns_) { throw std::out_of_range{"Matrix index is outside its dimensions."}; @@ -104,53 +102,11 @@ const double& Matrix::operator()(const std::size_t row, } Vector Matrix::Multiply(const Vector& rhs) const { - if (columns_ != rhs.Size()) { - throw std::invalid_argument{ - "Matrix-vector multiplication has incompatible dimensions."}; - } - for (const double value : values_) - if (!std::isfinite(value)) - throw std::invalid_argument{"Matrix product inputs must be finite."}; - for (std::size_t i = 0; i < rhs.Size(); ++i) - if (!std::isfinite(rhs.Data()[i])) - throw std::invalid_argument{"Matrix product inputs must be finite."}; - - Vector result{rows_}; - if (rows_ == 0 || columns_ == 0) { - return result; - } - - // The owned layout is row-major, so the leading dimension is the column - // count for the adapter call and remains invisible to public consumers. - cblas_dgemv(CblasRowMajor, CblasNoTrans, MklSizeOrThrow(rows_), - MklSizeOrThrow(columns_), 1.0, values_.data(), - MklSizeOrThrow(columns_), rhs.Data(), 1, 0.0, result.Data(), 1); - return result; + return dense_blas_internal::Multiply(*this, rhs); } Matrix Matrix::Multiply(const Matrix& rhs) const { - if (columns_ != rhs.rows_) { - throw std::invalid_argument{ - "Matrix multiplication has incompatible dimensions."}; - } - for (const double value : values_) - if (!std::isfinite(value)) - throw std::invalid_argument{"Matrix product inputs must be finite."}; - for (const double value : rhs.values_) - if (!std::isfinite(value)) - throw std::invalid_argument{"Matrix product inputs must be finite."}; - - Matrix result{rows_, rhs.columns_}; - if (rows_ == 0 || columns_ == 0 || rhs.columns_ == 0) { - return result; - } - - cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, MklSizeOrThrow(rows_), - MklSizeOrThrow(rhs.columns_), MklSizeOrThrow(columns_), 1.0, - values_.data(), MklSizeOrThrow(columns_), rhs.values_.data(), - MklSizeOrThrow(rhs.columns_), 0.0, result.values_.data(), - MklSizeOrThrow(rhs.columns_)); - return result; + return dense_blas_internal::Multiply(*this, rhs); } } // namespace fesa diff --git a/src/fesa/math/sparse_matrix.cpp b/src/fesa/math/sparse_matrix.cpp index 988964e..b2d9c16 100644 --- a/src/fesa/math/sparse_matrix.cpp +++ b/src/fesa/math/sparse_matrix.cpp @@ -10,7 +10,6 @@ #include -#include "fesa/fem/dof_manager.h" #include "math/dense_blas_internal.h" namespace fesa { diff --git a/src/fesa/math/vector.cpp b/src/fesa/math/vector.cpp index feadd38..b60da8f 100644 --- a/src/fesa/math/vector.cpp +++ b/src/fesa/math/vector.cpp @@ -1,26 +1,11 @@ #include "fesa/math/vector.h" -#include - #include #include #include "math/dense_blas_internal.h" namespace fesa { -namespace { - -/// @brief Preserves the Vector exception contract around private size -/// conversion. -MKL_INT MklSizeOrThrow(const std::size_t size) { - const auto converted = dense_blas_internal::ToMklSize(size); - if (!converted.HasValue()) { - throw std::length_error{"Dense vector size exceeds the MKL integer range."}; - } - return converted.Value(); -} - -} // namespace Vector::Vector(const std::size_t size, const double value) : values_(size, value) {} @@ -71,38 +56,22 @@ double Vector::Dot(const Vector& rhs) const { throw std::invalid_argument{ "Vector dot product requires equal dimensions."}; } - if (values_.empty()) { - return 0.0; - } - - return cblas_ddot(MklSizeOrThrow(Size()), Data(), 1, rhs.Data(), 1); + return dense_blas_internal::DotValues(Data(), Size(), rhs.Data()); } double Vector::Norm() const { - if (values_.empty()) { - return 0.0; - } - - return cblas_dnrm2(MklSizeOrThrow(Size()), Data(), 1); + return dense_blas_internal::NormValues(Data(), Size()); } void Vector::Scale(const double alpha) { - if (values_.empty()) { - return; - } - - cblas_dscal(MklSizeOrThrow(Size()), alpha, Data(), 1); + dense_blas_internal::ScaleValues(alpha, Data(), Size()); } void Vector::Axpy(const double alpha, const Vector& x) { if (Size() != x.Size()) { throw std::invalid_argument{"Vector axpy requires equal dimensions."}; } - if (values_.empty()) { - return; - } - - cblas_daxpy(MklSizeOrThrow(Size()), alpha, x.Data(), 1, Data(), 1); + dense_blas_internal::AxpyValues(alpha, x.Data(), Size(), Data()); } } // namespace fesa diff --git a/src/fesa/math/vector3_operations.h b/src/fesa/math/vector3_operations.h new file mode 100644 index 0000000..06daf7c --- /dev/null +++ b/src/fesa/math/vector3_operations.h @@ -0,0 +1,57 @@ +#ifndef FESA_MATH_VECTOR3_OPERATIONS_H_ +#define FESA_MATH_VECTOR3_OPERATIONS_H_ + +#include +#include + +#include "fesa/math/matrix.h" +#include "fesa/math/vector3.h" + +namespace fesa::math_internal { + +/// @brief Reduces four source-ordered geometric vectors with scalar-left products. +inline Vector3 WeightedSum(const std::array& weights, + const std::array& values) { + Vector3 result{}; + for (std::size_t node = 0U; node < 4U; ++node) { + result = result + weights[node] * values[node]; + } + return result; +} + +/// @brief Cancels the first-node anchor before the same ordered reduction. +inline Vector3 DerivativeSum(const std::array& derivatives, + const std::array& values) { + std::array relative{}; + for (std::size_t node = 0U; node < 4U; ++node) { + relative[node] = values[node] - values[0U]; + } + return WeightedSum(derivatives, relative); +} + +/// @brief Forms the positive cross-product matrix in fixed Cartesian coordinates. +inline Matrix Skew(const Vector3& vector) { + Matrix result{3U, 3U}; + result(0U, 1U) = -vector.Z(); + result(0U, 2U) = vector.Y(); + result(1U, 0U) = vector.Z(); + result(1U, 2U) = -vector.X(); + result(2U, 0U) = -vector.Y(); + result(2U, 1U) = vector.X(); + return result; +} + +/// @brief Applies a fixed 3-by-3 geometric frame with the original component graph. +inline Vector3 Multiply(const Matrix& matrix, const Vector3& vector) { + return Vector3{ + matrix(0U, 0U) * vector.X() + matrix(0U, 1U) * vector.Y() + + matrix(0U, 2U) * vector.Z(), + matrix(1U, 0U) * vector.X() + matrix(1U, 1U) * vector.Y() + + matrix(1U, 2U) * vector.Z(), + matrix(2U, 0U) * vector.X() + matrix(2U, 1U) * vector.Y() + + matrix(2U, 2U) * vector.Z()}; +} + +} // namespace fesa::math_internal + +#endif // FESA_MATH_VECTOR3_OPERATIONS_H_ diff --git a/src/fesa/model/shell_geometry.cpp b/src/fesa/model/shell_geometry.cpp index 9e152fe..3998ad7 100644 --- a/src/fesa/model/shell_geometry.cpp +++ b/src/fesa/model/shell_geometry.cpp @@ -7,9 +7,15 @@ #include "fesa/math/vector3.h" +#include "math/dense_blas_internal.h" +#include "math/vector3_operations.h" + namespace fesa { namespace { +using math_internal::DerivativeSum; +using math_internal::WeightedSum; + constexpr std::array kXiSigns{-1.0, 1.0, 1.0, -1.0}; constexpr std::array kEtaSigns{-1.0, -1.0, 1.0, 1.0}; @@ -38,27 +44,6 @@ ShapeData ShapeDataAt(double xi, double eta) { return data; } -Vector3 WeightedSum(const std::array& weights, - const std::array& values) { - Vector3 result{}; - for (std::size_t node = 0U; node < values.size(); ++node) { - result = result + weights[node] * values[node]; - } - return result; -} - -/// @brief Forms a translation-invariant covariant tangent in node order. -Vector3 DerivativeSum(const std::array& derivatives, - const std::array& coordinates) { - // Shape derivatives sum to zero, so translating by node 1 improves the - // numerical cancellation without changing the covariant tangent. - std::array relative{}; - for (std::size_t node = 0U; node < coordinates.size(); ++node) { - relative[node] = coordinates[node] - coordinates[0]; - } - return WeightedSum(derivatives, relative); -} - Result GeometryFailure(std::string code, const SourceLocation& location, std::string keyword, std::string identity, @@ -405,14 +390,27 @@ Result PreprocessShellGeometry( element.source_id.source_label_text, "Shell Jacobian is nonfinite or nonpositive at a required point."); } - const Vector3 reciprocal_xi = - (1.0 / jacobian) * covariant_eta.Cross(covariant_zeta); - const Vector3 reciprocal_eta = - (1.0 / jacobian) * covariant_zeta.Cross(covariant_xi); - const Vector3 reciprocal_zeta = - (1.0 / jacobian) * covariant_xi.Cross(covariant_eta); - if (!reciprocal_xi.IsFinite() || !reciprocal_eta.IsFinite() || - !reciprocal_zeta.IsFinite()) { + bool finite_actions = std::isfinite(1.0 / jacobian) && + covariant_eta.Cross(covariant_zeta).IsFinite() && + covariant_zeta.Cross(covariant_xi).IsFinite() && + covariant_xi.Cross(covariant_eta).IsFinite(); + if (finite_actions) { + Matrix coefficients{3U, 3U}; + for (std::size_t i = 0; i < 3U; ++i) { + coefficients(i, 0) = covariant_xi[i]; + coefficients(i, 1) = covariant_eta[i]; + coefficients(i, 2) = covariant_zeta[i]; + } + const auto factor = dense_blas_internal::DenseFactor::Lu(coefficients); + finite_actions = factor.HasValue(); + Matrix rhs{3U, 1U}; + for (std::size_t axis = 0; finite_actions && axis < 3U; ++axis) { + if (axis != 0U) rhs(axis - 1U, 0U) = 0.0; + rhs(axis, 0U) = 1.0; + finite_actions = factor.Value().Solve(rhs, true).HasValue(); + } + } + if (!finite_actions) { return GeometryFailure( "invalid-shell-jacobian", element.location, "ELEMENT", element.source_id.source_label_text, diff --git a/src/fesa/model/source_target_resolver.cpp b/src/fesa/model/source_target_resolver.cpp index fd81c01..df77c54 100644 --- a/src/fesa/model/source_target_resolver.cpp +++ b/src/fesa/model/source_target_resolver.cpp @@ -74,20 +74,21 @@ SourceTargetIndex::SourceTargetIndex( std::vector entries) : entries_{std::move(entries)} {} -SourceTargetIndex SourceTargetIndex::FromDomain(const Domain& domain) { +SourceTargetIndex SourceTargetIndex::FromNodeDefinitions( + const std::vector& nodes, const std::vector& node_sets) { std::vector entries; std::size_t declaration_order = 0U; - for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) { - const auto& node = domain.Nodes()[index]; + for (std::size_t index = 0U; index < nodes.size(); ++index) { + const auto& node = nodes[index]; entries.push_back({SourceEntityKind::kNode, node.source_id.instance_name, "", node.source_id, static_cast(index), declaration_order++}); } - for (const auto& set : domain.NodeSets()) { + for (const auto& set : node_sets) { for (const EntityIndex node_index : set.node_indices) { SourceEntityId source_id{set.instance_name.value_or(""), 0, ""}; - if (node_index < domain.Nodes().size()) { - source_id = domain.Nodes()[node_index].source_id; + if (node_index < nodes.size()) { + source_id = nodes[node_index].source_id; } entries.push_back({SourceEntityKind::kNode, set.instance_name.value_or(source_id.instance_name), @@ -96,6 +97,13 @@ SourceTargetIndex SourceTargetIndex::FromDomain(const Domain& domain) { } } + return SourceTargetIndex{std::move(entries)}; +} + +SourceTargetIndex SourceTargetIndex::FromDomain(const Domain& domain) { + SourceTargetIndex result = FromNodeDefinitions(domain.Nodes(), domain.NodeSets()); + auto& entries = result.entries_; + std::size_t declaration_order = entries.size(); for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { const auto& element = domain.Elements()[index]; entries.push_back({SourceEntityKind::kElement, @@ -114,7 +122,7 @@ SourceTargetIndex SourceTargetIndex::FromDomain(const Domain& domain) { declaration_order++}); } } - return SourceTargetIndex{std::move(entries)}; + return result; } const std::vector& SourceTargetIndex::Entries() diff --git a/src/fesa/results/prepared_results_writer.cpp b/src/fesa/results/prepared_results_writer.cpp new file mode 100644 index 0000000..aa69c41 --- /dev/null +++ b/src/fesa/results/prepared_results_writer.cpp @@ -0,0 +1,83 @@ +#include "fesa/results/prepared_results_writer.h" + +#include "fesa/results/dynamic_results_writer.h" +#include "fesa/results/eigen_results_writer.h" +#include "fesa/results/nonlinear_results_writer.h" +#include "fesa/results/results_writer.h" +#include "fesa/results/solid_results_writer.h" +#include "fesa/results/thermal_results_writer.h" + +namespace fesa { + +Status WritePreparedAnalysisResult( + const std::filesystem::path& results_path, const Domain& domain, + const PreparedAnalysisOutcome& outcome, + const std::vector& diagnostics, ResultsWriter& linear_writer, + NonlinearResultsWriter& nonlinear_writer, EigenResultsWriter& eigen_writer, + DynamicResultsWriter* dynamic_writer, ThermalResultsWriter* thermal_writer, + SolidResultsWriter* solid_writer) { + if (std::holds_alternative(outcome) || + std::holds_alternative(outcome) || + std::holds_alternative(outcome)) { + if (solid_writer == nullptr) + return Status::Failure(FailureCategory::kOutput, + {{Severity::kError, + "missing-solid-writer", + {}, + "", + {}, + "Solid result writer is required."}}); + if (const auto* result = std::get_if(&outcome)) + return solid_writer->Write(results_path, domain, result->state, + diagnostics); + if (const auto* result = std::get_if(&outcome)) + return solid_writer->Write(results_path, domain, *result, + diagnostics); + return solid_writer->Write( + results_path, domain, + std::get(outcome).state, diagnostics); + } + if (std::holds_alternative(outcome)) { + if (thermal_writer == nullptr) { + return Status::Failure(FailureCategory::kOutput, + {{Severity::kError, + "missing-thermal-writer", + {}, + "HEAT TRANSFER", + {}, + "Thermal result writer is required."}}); + } + return thermal_writer->Write( + results_path, domain, + std::get(outcome).state, diagnostics); + } + if (std::holds_alternative(outcome)) { + if (dynamic_writer == nullptr) + return Status::Failure(FailureCategory::kOutput, + {{Severity::kError, + "missing-dynamic-writer", + {}, + "DYNAMIC", + {}, + "Dynamic result writer is required."}}); + return dynamic_writer->Write( + results_path, domain, + std::get(outcome).state, diagnostics); + } + if (std::holds_alternative(outcome)) { + return eigen_writer.Write(results_path, domain, + std::get(outcome), + diagnostics); + } + if (std::holds_alternative(outcome)) { + return linear_writer.Write( + results_path, domain, + std::get(outcome).state, diagnostics); + } + + return nonlinear_writer.Write( + results_path, domain, + std::get(outcome).state, diagnostics); +} + +} // namespace fesa diff --git a/src/fesa/results/recovery_candidate.cpp b/src/fesa/results/recovery_candidate.cpp index d16300c..762931d 100644 --- a/src/fesa/results/recovery_candidate.cpp +++ b/src/fesa/results/recovery_candidate.cpp @@ -22,18 +22,11 @@ Status RecoveryFailure(const std::string& code, const SourceLocation& location, bool SameSourceIdentity(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; + return fesa::SourceIdentityEquals(left, right); } bool IsFiniteVector(const Vector& values) { - for (std::size_t index = 0U; index < values.Size(); ++index) { - if (!std::isfinite(values[index])) { - return false; - } - } - return true; + return math_internal::IsFinite(values); } } // namespace fesa::results_internal diff --git a/src/fesa/results/recovery_candidate.h b/src/fesa/results/recovery_candidate.h index ab11fcd..9141e62 100644 --- a/src/fesa/results/recovery_candidate.h +++ b/src/fesa/results/recovery_candidate.h @@ -1,9 +1,7 @@ #ifndef FESA_RESULTS_RECOVERY_CANDIDATE_H_ #define FESA_RESULTS_RECOVERY_CANDIDATE_H_ -#include #include -#include #include #include #include @@ -12,6 +10,7 @@ #include "fesa/core/status.h" #include "fesa/math/vector.h" #include "fesa/results/result_records.h" +#include "math/finite_values.h" namespace fesa::results_internal { @@ -53,8 +52,7 @@ bool SameSourceIdentity(const SourceEntityId& left, /// @brief Tests a fixed-size recovery component array for finite values. template bool IsFiniteArray(const std::array& values) { - return std::all_of(values.begin(), values.end(), - [](const double value) { return std::isfinite(value); }); + return math_internal::IsFinite(values); } /// @brief Tests an owned Vector for finite values. diff --git a/src/fesa/results/result_recovery.cpp b/src/fesa/results/result_recovery.cpp index b23041a..6ffe396 100644 --- a/src/fesa/results/result_recovery.cpp +++ b/src/fesa/results/result_recovery.cpp @@ -32,21 +32,8 @@ constexpr double kAxisTolerance = 1.0e-12; using AxisSet = std::array, 3>; -Status RecoveryFailure(const std::string& code, const SourceLocation& location, - const std::string& identity, - const std::string& message) { - return Status::Failure(FailureCategory::kModel, - {{Severity::kError, code, location, "RESULT_RECOVERY", - identity, message}}); -} - -template -Result RecoveryResultFailure(const std::string& code, - const SourceLocation& location, - const std::string& identity, - const std::string& message) { - return Result::Failure(RecoveryFailure(code, location, identity, message)); -} +using results_internal::RecoveryFailure; +using results_internal::RecoveryResultFailure; template bool IsFinite(const std::array& values) { diff --git a/src/fesa/results/shell_result_recovery.cpp b/src/fesa/results/shell_result_recovery.cpp index bd47684..b7c00da 100644 --- a/src/fesa/results/shell_result_recovery.cpp +++ b/src/fesa/results/shell_result_recovery.cpp @@ -5,7 +5,6 @@ #include namespace fesa::results_internal { -namespace { bool IsFiniteShellRow(const ShellResultRow& row) { if (!IsFiniteArray(row.natural_coordinates) || @@ -25,8 +24,6 @@ bool IsFiniteShellRow(const ShellResultRow& row) { }); } -} // namespace - Status ShellResultRecovery(const SourceLocation& location, const SourceEntityId& source_id, const EntityIndex expected_element, diff --git a/src/fesa/results/shell_result_recovery.h b/src/fesa/results/shell_result_recovery.h index 183eba4..01bc186 100644 --- a/src/fesa/results/shell_result_recovery.h +++ b/src/fesa/results/shell_result_recovery.h @@ -8,6 +8,9 @@ namespace fesa::results_internal { +/// @brief Checks physical fields in their preserved source-row order. +bool IsFiniteShellRow(const ShellResultRow& row); + /// @brief Validates and appends physical shell rows and energy evidence. Status ShellResultRecovery(const SourceLocation& location, const SourceEntityId& source_id, diff --git a/src/fesa/solvers/eigen/mkl_feast_eigen_solver.cpp b/src/fesa/solvers/eigen/mkl_feast_eigen_solver.cpp index 7e3a237..e97674d 100644 --- a/src/fesa/solvers/eigen/mkl_feast_eigen_solver.cpp +++ b/src/fesa/solvers/eigen/mkl_feast_eigen_solver.cpp @@ -355,11 +355,12 @@ bool FillInitialBasis(std::vector& vectors, const std::size_t n, } for (double& value : reflector) value /= reflector_norm; for (std::size_t target = column; target < columns; ++target) { - const auto length = static_cast(n - column); + const std::size_t length = n - column; auto* target_column = vectors.data() + column + target * n; - const double dot = cblas_ddot(length, reflector.data(), 1, - target_column, 1); - cblas_daxpy(length, -2.0 * dot, reflector.data(), 1, target_column, 1); + const double dot = dense_blas_internal::DotValues( + reflector.data(), length, target_column); + dense_blas_internal::AxpyValues(-2.0 * dot, reflector.data(), length, + target_column); } reflectors.push_back(std::move(reflector)); } @@ -370,11 +371,12 @@ bool FillInitialBasis(std::vector& vectors, const std::size_t n, const std::size_t column = index - 1U; const auto& reflector = reflectors[column]; for (std::size_t target = 0U; target < columns; ++target) { - const auto length = static_cast(n - column); + const std::size_t length = n - column; auto* target_column = vectors.data() + column + target * n; - const double dot = cblas_ddot(length, reflector.data(), 1, - target_column, 1); - cblas_daxpy(length, -2.0 * dot, reflector.data(), 1, target_column, 1); + const double dot = dense_blas_internal::DotValues( + reflector.data(), length, target_column); + dense_blas_internal::AxpyValues(-2.0 * dot, reflector.data(), length, + target_column); } } for (std::size_t column = 0U; column < columns; ++column) @@ -383,9 +385,8 @@ bool FillInitialBasis(std::vector& vectors, const std::size_t n, double gram_error = 0.0; for (std::size_t left = 0U; left < columns; ++left) { for (std::size_t right = 0U; right < columns; ++right) { - const double dot = cblas_ddot(static_cast(n), - vectors.data() + left * n, 1, - vectors.data() + right * n, 1); + const double dot = dense_blas_internal::DotValues( + vectors.data() + left * n, n, vectors.data() + right * n); gram_error = std::hypot(gram_error, dot - (left == right ? 1.0 : 0.0)); } } diff --git a/src/fesa/solvers/linear/mkl_pardiso_solver.cpp b/src/fesa/solvers/linear/mkl_pardiso_solver.cpp index 0cc9adc..edf319a 100644 --- a/src/fesa/solvers/linear/mkl_pardiso_solver.cpp +++ b/src/fesa/solvers/linear/mkl_pardiso_solver.cpp @@ -1,339 +1,15 @@ #include "fesa/solvers/linear/mkl_pardiso_solver.h" -#include - -#include -#include -#include -#include -#include #include -#include -#include -#include -#include "fesa/math/sparse_matrix.h" -#include "fesa/math/vector.h" -#include "math/dense_blas_internal.h" +#include "solvers/linear/pardiso_session.h" namespace fesa { -namespace { - -/// @brief Builds a structured linear-solver failure. -Status SolverFailure(const std::string& code, const std::string& identity, - const std::string& message) { - return Status::Failure( - FailureCategory::kSolver, - {{Severity::kError, code, {{}, 0U}, "PARDISO", identity, message}}); -} - -/// @brief Translates a PARDISO phase error into the stable solver taxonomy. -Status PardisoFailure(const MKL_INT phase, const MKL_INT error) { - std::string code; - std::string reason; - switch (error) { - case -4: - code = "pardiso-zero-or-negative-pivot"; - reason = "zero or negative pivot"; - break; - case -7: - code = "pardiso-singular-diagonal"; - reason = "singular diagonal"; - break; - case -8: - code = "pardiso-integer-overflow"; - reason = "32-bit backend integer overflow"; - break; - case 21: - case 22: - case 23: - case 24: - code = "pardiso-invalid-csr"; - reason = "matrix checker rejected the CSR indices"; - break; - default: - code = phase == 11 ? "pardiso-analysis-failed" - : phase == 22 ? "pardiso-factorization-failed" - : phase == 33 ? "pardiso-solve-failed" - : "pardiso-release-failed"; - reason = "backend error"; - break; - } - - const std::string phase_text = std::to_string(phase); - const std::string error_text = std::to_string(error); - return SolverFailure(code, "phase=" + phase_text + ",error=" + error_text, - "oneMKL PARDISO phase " + phase_text + - " failed with error " + error_text + " (" + reason + - ")."); -} - -/// @brief Reports whether a public size fits the private MKL integer type. -bool ConvertsToMklInt(const std::size_t value) { - return value <= - static_cast((std::numeric_limits::max)()); -} - -} // namespace class MklPardisoSolver::Impl { public: - Impl() = default; - - ~Impl() { static_cast(Release()); } - - /// @brief Validates and factorizes one public CSR matrix. - Status Factorize(const SparseMatrix& matrix) { - const MKL_INT release_error = Release(); - if (release_error != 0) { - return PardisoFailure(-1, release_error); - } - - const Status csr_status = matrix.Validate(); - if (!csr_status.IsOk()) { - return SolverFailure("solver-invalid-csr", "public-csr", - "The public sparse matrix failed CSR validation."); - } - if (matrix.Rows() != matrix.Columns()) { - return SolverFailure("solver-matrix-not-square", "matrix-shape", - "PARDISO factorization requires a square matrix."); - } - if (matrix.Rows() == 0U) { - // A fully constrained model has no free equations. Preserve the - // observable factorize/solve lifecycle without creating backend - // state or calling PARDISO with its invalid n=0 input. - factorized_ = true; - return Status::Ok(); - } - if (!ConvertsToMklInt(matrix.Rows()) || - !ConvertsToMklInt(matrix.Values().size())) { - return SolverFailure( - "solver-dimension-overflow", "matrix-shape", - "Sparse matrix dimensions exceed the oneMKL integer range."); - } - - const Status copy_status = CopyValidatedUpperTriangle(matrix); - if (!copy_status.IsOk()) { - ClearOwnedArrays(); - return copy_status; - } - - // PARDISO owns internal memory behind pt after phase 11. Initialize - // once per factorization and retain it until refactorization/destruction. - pt_.fill(nullptr); - iparm_.fill(0); - pardisoinit(pt_.data(), &mtype_, iparm_.data()); - iparm_[26] = 1; // Validate sorted CSR integer arrays. - iparm_[34] = 1; // Consume the project's native zero-based CSR. - permutation_.assign(static_cast(equation_count_), 0); - owns_pardiso_state_ = true; - - MKL_INT phase = 11; - MKL_INT error = 0; - CallPardiso(phase, nullptr, nullptr, error); - if (error != 0) { - const Status failure = PardisoFailure(phase, error); - static_cast(Release()); - return failure; - } - - phase = 22; - error = 0; - CallPardiso(phase, nullptr, nullptr, error); - if (error != 0) { - const Status failure = PardisoFailure(phase, error); - static_cast(Release()); - return failure; - } - - factorized_ = true; - return Status::Ok(); - } - - /// @brief Substitutes one RHS while preserving solution on failure. - Status Solve(const Vector& rhs, Vector& solution) { - if (!factorized_) { - return SolverFailure( - "solver-not-factorized", "factorization-state", - "Substitution requires a successful retained factorization."); - } - const std::size_t size = static_cast(equation_count_); - if (rhs.Size() != size || solution.Size() != size) { - return SolverFailure( - "solver-vector-dimension-mismatch", "rhs-or-solution", - "RHS and solution dimensions must match the factorized matrix."); - } - for (std::size_t index = 0U; index < rhs.Size(); ++index) { - if (!std::isfinite(rhs[index])) { - return SolverFailure("nonfinite-solver-rhs", std::to_string(index), - "PARDISO RHS values must be finite."); - } - } - if (size == 0U) { - return Status::Ok(); - } - - std::vector rhs_copy(rhs.Data(), rhs.Data() + rhs.Size()); - Vector candidate{size}; - MKL_INT phase = 33; - MKL_INT error = 0; - CallPardiso(phase, rhs_copy.data(), candidate.Data(), error); - if (error != 0) { - return PardisoFailure(phase, error); - } - for (std::size_t index = 0U; index < candidate.Size(); ++index) { - if (!std::isfinite(candidate[index])) { - return SolverFailure( - "nonfinite-solver-solution", std::to_string(index), - "PARDISO substitution produced a nonfinite solution."); - } - } - - solution = std::move(candidate); - return Status::Ok(); - } - - private: - /// @brief Validates symmetry and copies the upper triangle for PARDISO. - Status CopyValidatedUpperTriangle(const SparseMatrix& matrix) { - const auto& public_offsets = matrix.RowOffsets(); - const auto& public_columns = matrix.ColumnIndices(); - const auto& public_values = matrix.Values(); - - double matrix_scale = 0.0; - for (const double value : public_values) { - matrix_scale = (std::max)(matrix_scale, std::abs(value)); - } - - for (std::size_t row = 0U; row < matrix.Rows(); ++row) { - for (std::size_t position = public_offsets[row]; - position < public_offsets[row + 1U]; ++position) { - const std::size_t column = public_columns[position]; - const auto reverse_begin = - public_columns.begin() + - static_cast(public_offsets[column]); - const auto reverse_end = - public_columns.begin() + - static_cast(public_offsets[column + 1U]); - const auto reverse = std::lower_bound(reverse_begin, reverse_end, row); - if (reverse == reverse_end || *reverse != row) { - return SolverFailure( - "solver-matrix-not-symmetric", - std::to_string(row) + ":" + std::to_string(column), - "The full public CSR must contain both symmetric entries."); - } - - const std::size_t reverse_position = static_cast( - std::distance(public_columns.begin(), reverse)); - const double left = public_values[position]; - const double right = public_values[reverse_position]; - const double difference = std::abs(left - right); - // The approved symmetry test is normalized by the matrix's - // actual nonzero scale, without an absolute unit-size floor. - const bool is_symmetric = matrix_scale == 0.0 - ? difference == 0.0 - : difference <= 1.0e-12 * matrix_scale; - if (!is_symmetric) { - return SolverFailure( - "solver-matrix-not-symmetric", - std::to_string(row) + ":" + std::to_string(column), - "The full public CSR values violate the approved symmetry " - "tolerance."); - } - } - } - - equation_count_ = static_cast(matrix.Rows()); - row_offsets_.clear(); - column_indices_.clear(); - values_.clear(); - row_offsets_.reserve(matrix.Rows() + 1U); - row_offsets_.push_back(0); - - for (std::size_t row = 0U; row < matrix.Rows(); ++row) { - bool has_diagonal = false; - for (std::size_t position = public_offsets[row]; - position < public_offsets[row + 1U]; ++position) { - const std::size_t column = public_columns[position]; - if (column < row) { - continue; - } - if (!ConvertsToMklInt(column) || - !ConvertsToMklInt(column_indices_.size())) { - return SolverFailure( - "solver-dimension-overflow", - std::to_string(row) + ":" + std::to_string(column), - "CSR indices exceed the oneMKL integer range."); - } - has_diagonal = has_diagonal || column == row; - column_indices_.push_back(static_cast(column)); - values_.push_back(public_values[position]); - } - if (!has_diagonal) { - return SolverFailure( - "solver-missing-diagonal", std::to_string(row), - "Every PARDISO SPD row must retain its diagonal slot."); - } - if (!ConvertsToMklInt(column_indices_.size())) { - return SolverFailure( - "solver-dimension-overflow", std::to_string(row), - "CSR row offsets exceed the oneMKL integer range."); - } - row_offsets_.push_back(static_cast(column_indices_.size())); - } - return Status::Ok(); - } - - /// @brief Calls PARDISO with retained private arrays and phase state. - void CallPardiso(const MKL_INT phase, double* rhs, double* solution, - MKL_INT& error) { - const dense_blas_internal::ScopedSingleEquationThreads thread_scope{ - static_cast(equation_count_)}; - pardiso(pt_.data(), &max_factorizations_, &matrix_number_, &mtype_, &phase, - &equation_count_, values_.data(), row_offsets_.data(), - column_indices_.data(), permutation_.data(), &rhs_count_, - iparm_.data(), &message_level_, rhs, solution, &error); - } - - /// @brief Releases backend state and resets every retained array. - MKL_INT Release() noexcept { - MKL_INT error = 0; - if (owns_pardiso_state_) { - const MKL_INT phase = -1; - double placeholder = 0.0; - CallPardiso(phase, &placeholder, &placeholder, error); - } - owns_pardiso_state_ = false; - factorized_ = false; - pt_.fill(nullptr); - iparm_.fill(0); - permutation_.clear(); - ClearOwnedArrays(); - return error; - } - - /// @brief Clears FESA-owned CSR arrays without touching backend state. - void ClearOwnedArrays() noexcept { - equation_count_ = 0; - row_offsets_.clear(); - column_indices_.clear(); - values_.clear(); - } - - std::array pt_{}; - std::array iparm_{}; - std::vector row_offsets_; - std::vector column_indices_; - std::vector permutation_; - std::vector values_; - MKL_INT equation_count_{0}; - MKL_INT max_factorizations_{1}; - MKL_INT matrix_number_{1}; - MKL_INT mtype_{2}; - MKL_INT rhs_count_{1}; - MKL_INT message_level_{0}; - bool owns_pardiso_state_{false}; - bool factorized_{false}; + pardiso_internal::PardisoSession session_{ + pardiso_internal::PardisoMatrixPolicy::kSpdUpper}; }; MklPardisoSolver::MklPardisoSolver() : impl_{std::make_unique()} {} @@ -341,11 +17,11 @@ MklPardisoSolver::MklPardisoSolver() : impl_{std::make_unique()} {} MklPardisoSolver::~MklPardisoSolver() = default; Status MklPardisoSolver::Factorize(const SparseMatrix& matrix) { - return impl_->Factorize(matrix); + return impl_->session_.Factorize(matrix); } Status MklPardisoSolver::Solve(const Vector& rhs, Vector& solution) const { - return impl_->Solve(rhs, solution); + return impl_->session_.Solve(rhs, solution); } } // namespace fesa diff --git a/src/fesa/solvers/linear/mkl_pardiso_tangent_solver.cpp b/src/fesa/solvers/linear/mkl_pardiso_tangent_solver.cpp index b544312..728dc53 100644 --- a/src/fesa/solvers/linear/mkl_pardiso_tangent_solver.cpp +++ b/src/fesa/solvers/linear/mkl_pardiso_tangent_solver.cpp @@ -1,297 +1,27 @@ #include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h" -#include - -#include -#include -#include -#include #include -#include -#include -#include -#include "fesa/math/sparse_matrix.h" -#include "fesa/math/vector.h" -#include "math/dense_blas_internal.h" +#include "solvers/linear/pardiso_session.h" namespace fesa { -namespace { - -/// @brief Builds a structured nonlinear tangent-solver failure. -Status SolverFailure(const std::string& code, const std::string& identity, - const std::string& message) { - return Status::Failure( - FailureCategory::kSolver, - {{Severity::kError, code, {{}, 0U}, "PARDISO", identity, message}}); -} - -/// @brief Translates a PARDISO phase error into the stable solver taxonomy. -Status PardisoFailure(const MKL_INT phase, const MKL_INT error) { - std::string code; - std::string reason; - switch (error) { - case -4: - code = "pardiso-zero-or-negative-pivot"; - reason = "zero or negative pivot"; - break; - case -7: - code = "pardiso-singular-diagonal"; - reason = "singular diagonal"; - break; - case -8: - code = "pardiso-integer-overflow"; - reason = "32-bit backend integer overflow"; - break; - case 21: - case 22: - case 23: - case 24: - code = "pardiso-invalid-csr"; - reason = "matrix checker rejected the CSR indices"; - break; - default: - code = phase == 11 ? "pardiso-analysis-failed" - : phase == 22 ? "pardiso-factorization-failed" - : phase == 33 ? "pardiso-solve-failed" - : "pardiso-release-failed"; - reason = "backend error"; - break; - } - - const std::string phase_text = std::to_string(phase); - const std::string error_text = std::to_string(error); - return SolverFailure(code, "phase=" + phase_text + ",error=" + error_text, - "oneMKL PARDISO phase " + phase_text + - " failed with error " + error_text + " (" + reason + - ")."); -} - -/// @brief Reports whether a public size fits the private MKL integer type. -bool ConvertsToMklInt(const std::size_t value) { - return value <= - static_cast((std::numeric_limits::max)()); -} - -} // namespace class MklPardisoTangentSolver::Impl { public: - Impl() = default; - - ~Impl() { static_cast(Release()); } - - /// @brief Replaces any retained state with one validated full-CSR tangent. - Status FactorizeFresh(const SparseMatrix& matrix) { - const MKL_INT release_error = Release(); - if (release_error != 0) { - return PardisoFailure(-1, release_error); - } - - const Status csr_status = matrix.Validate(); - if (!csr_status.IsOk()) { - return SolverFailure("solver-invalid-csr", "public-csr", - "The public sparse matrix failed CSR validation."); - } - if (matrix.Rows() != matrix.Columns()) { - return SolverFailure("solver-matrix-not-square", "matrix-shape", - "PARDISO factorization requires a square matrix."); - } - if (matrix.Rows() == 0U) { - // PARDISO does not accept n=0, but a fully constrained nonlinear - // correction remains a valid factorize/solve lifecycle. - factorized_ = true; - return Status::Ok(); - } - if (!ConvertsToMklInt(matrix.Rows()) || - !ConvertsToMklInt(matrix.Values().size())) { - return SolverFailure( - "solver-dimension-overflow", "matrix-shape", - "Sparse matrix dimensions exceed the oneMKL integer range."); - } - - const Status copy_status = CopyValidatedFullCsr(matrix); - if (!copy_status.IsOk()) { - ClearOwnedArrays(); - return copy_status; - } - - // The nonlinear tangent is generally nonsymmetric, so PARDISO consumes - // the complete public CSR with mtype=11 instead of an SPD triangle. - pt_.fill(nullptr); - iparm_.fill(0); - pardisoinit(pt_.data(), &mtype_, iparm_.data()); - iparm_[26] = 1; // Validate sorted CSR integer arrays. - iparm_[34] = 1; // Consume the project's native zero-based CSR. - permutation_.assign(static_cast(equation_count_), 0); - owns_pardiso_state_ = true; - - MKL_INT phase = 11; - MKL_INT error = 0; - CallPardiso(phase, nullptr, nullptr, error); - if (error != 0) { - const Status failure = PardisoFailure(phase, error); - static_cast(Release()); - return failure; - } - - phase = 22; - error = 0; - CallPardiso(phase, nullptr, nullptr, error); - if (error != 0) { - const Status failure = PardisoFailure(phase, error); - static_cast(Release()); - return failure; - } - if (iparm_[13] != 0) { - const Status failure = SolverFailure( - "pardiso-perturbed-pivot", "phase=22", - "oneMKL PARDISO perturbed one or more tangent pivots."); - static_cast(Release()); - return failure; - } - - factorized_ = true; - return Status::Ok(); - } - - /// @brief Solves one correction while preserving caller output on failure. - Status SolveCorrection(const Vector& rhs, Vector& correction) { - if (!factorized_) { - return SolverFailure( - "solver-not-factorized", "factorization-state", - "Substitution requires a successful retained factorization."); - } - const std::size_t size = static_cast(equation_count_); - if (rhs.Size() != size || correction.Size() != size) { - return SolverFailure( - "solver-vector-dimension-mismatch", "rhs-or-correction", - "RHS and correction dimensions must match the factorized matrix."); - } - for (std::size_t index = 0U; index < rhs.Size(); ++index) { - if (!std::isfinite(rhs[index])) { - return SolverFailure("nonfinite-solver-rhs", std::to_string(index), - "PARDISO RHS values must be finite."); - } - } - if (size == 0U) { - return Status::Ok(); - } - - std::vector rhs_copy(rhs.Data(), rhs.Data() + rhs.Size()); - Vector candidate{size}; - MKL_INT phase = 33; - MKL_INT error = 0; - CallPardiso(phase, rhs_copy.data(), candidate.Data(), error); - if (error != 0) { - return PardisoFailure(phase, error); - } - for (std::size_t index = 0U; index < candidate.Size(); ++index) { - if (!std::isfinite(candidate[index])) { - return SolverFailure( - "nonfinite-solver-solution", std::to_string(index), - "PARDISO substitution produced a nonfinite solution."); - } - } - - correction = std::move(candidate); - return Status::Ok(); - } - - private: - /// @brief Copies validated public CSR without symmetry or diagonal filtering. - Status CopyValidatedFullCsr(const SparseMatrix& matrix) { - equation_count_ = static_cast(matrix.Rows()); - row_offsets_.clear(); - column_indices_.clear(); - values_.clear(); - row_offsets_.reserve(matrix.RowOffsets().size()); - column_indices_.reserve(matrix.ColumnIndices().size()); - values_.reserve(matrix.Values().size()); - - for (const std::size_t offset : matrix.RowOffsets()) { - if (!ConvertsToMklInt(offset)) { - return SolverFailure("solver-dimension-overflow", "row-offsets", - "CSR row offsets exceed the oneMKL integer " - "range."); - } - row_offsets_.push_back(static_cast(offset)); - } - for (const std::size_t column : matrix.ColumnIndices()) { - if (!ConvertsToMklInt(column)) { - return SolverFailure("solver-dimension-overflow", "column-indices", - "CSR indices exceed the oneMKL integer range."); - } - column_indices_.push_back(static_cast(column)); - } - values_ = matrix.Values(); - return Status::Ok(); - } - - /// @brief Calls PARDISO with retained private arrays and phase state. - void CallPardiso(const MKL_INT phase, double* rhs, double* correction, - MKL_INT& error) { - const dense_blas_internal::ScopedSingleEquationThreads thread_scope{ - static_cast(equation_count_)}; - pardiso(pt_.data(), &max_factorizations_, &matrix_number_, &mtype_, &phase, - &equation_count_, values_.data(), row_offsets_.data(), - column_indices_.data(), permutation_.data(), &rhs_count_, - iparm_.data(), &message_level_, rhs, correction, &error); - } - - /// @brief Releases backend state and resets every retained array. - MKL_INT Release() noexcept { - MKL_INT error = 0; - if (owns_pardiso_state_) { - const MKL_INT phase = -1; - double placeholder = 0.0; - CallPardiso(phase, &placeholder, &placeholder, error); - } - owns_pardiso_state_ = false; - factorized_ = false; - pt_.fill(nullptr); - iparm_.fill(0); - permutation_.clear(); - ClearOwnedArrays(); - return error; - } - - /// @brief Clears FESA-owned CSR arrays without touching backend state. - void ClearOwnedArrays() noexcept { - equation_count_ = 0; - row_offsets_.clear(); - column_indices_.clear(); - values_.clear(); - } - - std::array pt_{}; - std::array iparm_{}; - std::vector row_offsets_; - std::vector column_indices_; - std::vector permutation_; - std::vector values_; - MKL_INT equation_count_{0}; - MKL_INT max_factorizations_{1}; - MKL_INT matrix_number_{1}; - MKL_INT mtype_{11}; - MKL_INT rhs_count_{1}; - MKL_INT message_level_{0}; - bool owns_pardiso_state_{false}; - bool factorized_{false}; + pardiso_internal::PardisoSession session_{ + pardiso_internal::PardisoMatrixPolicy::kUnsymmetricFull}; }; -MklPardisoTangentSolver::MklPardisoTangentSolver() - : impl_{std::make_unique()} {} +MklPardisoTangentSolver::MklPardisoTangentSolver() : impl_{std::make_unique()} {} MklPardisoTangentSolver::~MklPardisoTangentSolver() = default; Status MklPardisoTangentSolver::FactorizeFresh(const SparseMatrix& matrix) { - return impl_->FactorizeFresh(matrix); + return impl_->session_.Factorize(matrix); } -Status MklPardisoTangentSolver::SolveCorrection(const Vector& rhs, - Vector& correction) const { - return impl_->SolveCorrection(rhs, correction); +Status MklPardisoTangentSolver::SolveCorrection(const Vector& rhs, Vector& solution) const { + return impl_->session_.Solve(rhs, solution); } } // namespace fesa diff --git a/src/fesa/solvers/linear/pardiso_session.cpp b/src/fesa/solvers/linear/pardiso_session.cpp new file mode 100644 index 0000000..b6fcfcc --- /dev/null +++ b/src/fesa/solvers/linear/pardiso_session.cpp @@ -0,0 +1,384 @@ +#include "solvers/linear/pardiso_session.h" + +#include +#include +#include +#include +#include + +#include "fesa/math/sparse_matrix.h" +#include "fesa/math/vector.h" +#include "math/dense_blas_internal.h" +#include "solvers/linear/pardiso_session_test_seam.h" + +namespace fesa { +namespace { +thread_local internal::PardisoBackendTestHook* g_pardiso_backend_test_hook = + nullptr; +} // namespace + +namespace internal { +ScopedPardisoBackendTestHook::ScopedPardisoBackendTestHook( + PardisoBackendTestHook hook) + : hook_(std::move(hook)), previous_(g_pardiso_backend_test_hook) { + g_pardiso_backend_test_hook = &hook_; +} + +ScopedPardisoBackendTestHook::~ScopedPardisoBackendTestHook() { + g_pardiso_backend_test_hook = previous_; +} +} // namespace internal + +namespace pardiso_internal { +namespace { +/// @brief Builds a structured linear-solver failure. +Status SolverFailure(const std::string& code, const std::string& identity, + const std::string& message) { + return Status::Failure( + FailureCategory::kSolver, + {{Severity::kError, code, {{}, 0U}, "PARDISO", identity, message}}); +} + +/// @brief Translates a PARDISO phase error into the stable solver taxonomy. +Status PardisoFailure(const MKL_INT phase, const MKL_INT error) { + std::string code; + std::string reason; + switch (error) { + case -4: + code = "pardiso-zero-or-negative-pivot"; + reason = "zero or negative pivot"; + break; + case -7: + code = "pardiso-singular-diagonal"; + reason = "singular diagonal"; + break; + case -8: + code = "pardiso-integer-overflow"; + reason = "32-bit backend integer overflow"; + break; + case 21: + case 22: + case 23: + case 24: + code = "pardiso-invalid-csr"; + reason = "matrix checker rejected the CSR indices"; + break; + default: + code = phase == 11 ? "pardiso-analysis-failed" + : phase == 22 ? "pardiso-factorization-failed" + : phase == 33 ? "pardiso-solve-failed" + : "pardiso-release-failed"; + reason = "backend error"; + break; + } + + const std::string phase_text = std::to_string(phase); + const std::string error_text = std::to_string(error); + return SolverFailure(code, "phase=" + phase_text + ",error=" + error_text, + "oneMKL PARDISO phase " + phase_text + + " failed with error " + error_text + " (" + reason + + ")."); +} + +/// @brief Reports whether a public size fits the private MKL integer type. +bool ConvertsToMklInt(const std::size_t value) { + return dense_blas_internal::ToMklSize(value).HasValue(); +} + +} // namespace + +PardisoSession::PardisoSession(const PardisoMatrixPolicy policy) + : policy_(policy), + mtype_(policy == PardisoMatrixPolicy::kSpdUpper ? 2 : 11) {} + +PardisoSession::~PardisoSession() { static_cast(Release()); } + +Status PardisoSession::Factorize(const SparseMatrix& matrix) { + const MKL_INT release_error = Release(); + if (release_error != 0) { + return PardisoFailure(-1, release_error); + } + + const Status csr_status = matrix.Validate(); + if (!csr_status.IsOk()) { + return SolverFailure("solver-invalid-csr", "public-csr", + "The public sparse matrix failed CSR validation."); + } + if (matrix.Rows() != matrix.Columns()) { + return SolverFailure("solver-matrix-not-square", "matrix-shape", + "PARDISO factorization requires a square matrix."); + } + if (matrix.Rows() == 0U) { + // A fully constrained model has no free equations. Preserve the + // observable factorize/solve lifecycle without creating backend + // state or calling PARDISO with its invalid n=0 input. + factorized_ = true; + return Status::Ok(); + } + if (!ConvertsToMklInt(matrix.Rows()) || + !ConvertsToMklInt(matrix.Values().size())) { + return SolverFailure( + "solver-dimension-overflow", "matrix-shape", + "Sparse matrix dimensions exceed the oneMKL integer range."); + } + + const Status copy_status = + policy_ == PardisoMatrixPolicy::kSpdUpper + ? CopyValidatedUpperTriangle(matrix) + : CopyValidatedFullCsr(matrix); + if (!copy_status.IsOk()) { + ClearOwnedArrays(); + return copy_status; + } + + // PARDISO owns internal memory behind pt after phase 11. Initialize + // once per factorization and retain it until refactorization/destruction. + pt_.fill(nullptr); + iparm_.fill(0); + pardisoinit(pt_.data(), &mtype_, iparm_.data()); + iparm_[26] = 1; // Validate sorted CSR integer arrays. + iparm_[34] = 1; // Consume the project's native zero-based CSR. + permutation_.assign(static_cast(equation_count_), 0); + owns_pardiso_state_ = true; + + try { + MKL_INT phase = 11; + MKL_INT error = 0; + CallPardiso(phase, nullptr, nullptr, error); + if (error != 0) { + const Status failure = PardisoFailure(phase, error); + static_cast(Release()); + return failure; + } + + phase = 22; + error = 0; + CallPardiso(phase, nullptr, nullptr, error); + if (error != 0) { + const Status failure = PardisoFailure(phase, error); + static_cast(Release()); + return failure; + } + + if (policy_ == PardisoMatrixPolicy::kUnsymmetricFull && iparm_[13] != 0) { + const Status failure = SolverFailure( + "pardiso-perturbed-pivot", "phase=22", + "oneMKL PARDISO perturbed one or more tangent pivots."); + static_cast(Release()); + return failure; + } + factorized_ = true; + return Status::Ok(); + } catch (...) { + static_cast(Release()); + throw; + } +} + +Status PardisoSession::Solve(const Vector& rhs, Vector& solution) { + if (!factorized_) { + return SolverFailure( + "solver-not-factorized", "factorization-state", + "Substitution requires a successful retained factorization."); + } + const std::size_t size = static_cast(equation_count_); + if (rhs.Size() != size || solution.Size() != size) { + return SolverFailure( + "solver-vector-dimension-mismatch", + policy_ == PardisoMatrixPolicy::kSpdUpper ? "rhs-or-solution" + : "rhs-or-correction", + policy_ == PardisoMatrixPolicy::kSpdUpper + ? "RHS and solution dimensions must match the factorized matrix." + : "RHS and correction dimensions must match the factorized matrix."); + } + for (std::size_t index = 0U; index < rhs.Size(); ++index) { + if (!std::isfinite(rhs[index])) { + return SolverFailure("nonfinite-solver-rhs", std::to_string(index), + "PARDISO RHS values must be finite."); + } + } + if (size == 0U) { + return Status::Ok(); + } + + std::vector rhs_copy(rhs.Size()); + dense_blas_internal::CopyValues(rhs.Data(), rhs.Size(), rhs_copy.data()); + Vector candidate{size}; + MKL_INT phase = 33; + MKL_INT error = 0; + CallPardiso(phase, rhs_copy.data(), candidate.Data(), error); + if (error != 0) { + return PardisoFailure(phase, error); + } + for (std::size_t index = 0U; index < candidate.Size(); ++index) { + if (!std::isfinite(candidate[index])) { + return SolverFailure( + "nonfinite-solver-solution", std::to_string(index), + "PARDISO substitution produced a nonfinite solution."); + } + } + + solution = std::move(candidate); + return Status::Ok(); +} + +Status PardisoSession::CopyValidatedUpperTriangle(const SparseMatrix& matrix) { + const auto& public_offsets = matrix.RowOffsets(); + const auto& public_columns = matrix.ColumnIndices(); + const auto& public_values = matrix.Values(); + + double matrix_scale = 0.0; + for (const double value : public_values) { + matrix_scale = (std::max)(matrix_scale, std::abs(value)); + } + + for (std::size_t row = 0U; row < matrix.Rows(); ++row) { + for (std::size_t position = public_offsets[row]; + position < public_offsets[row + 1U]; ++position) { + const std::size_t column = public_columns[position]; + const auto reverse_begin = + public_columns.begin() + + static_cast(public_offsets[column]); + const auto reverse_end = + public_columns.begin() + + static_cast(public_offsets[column + 1U]); + const auto reverse = std::lower_bound(reverse_begin, reverse_end, row); + if (reverse == reverse_end || *reverse != row) { + return SolverFailure( + "solver-matrix-not-symmetric", + std::to_string(row) + ":" + std::to_string(column), + "The full public CSR must contain both symmetric entries."); + } + + const std::size_t reverse_position = static_cast( + std::distance(public_columns.begin(), reverse)); + const double left = public_values[position]; + const double right = public_values[reverse_position]; + const double difference = std::abs(left - right); + // The approved symmetry test is normalized by the matrix's + // actual nonzero scale, without an absolute unit-size floor. + const bool is_symmetric = matrix_scale == 0.0 + ? difference == 0.0 + : difference <= 1.0e-12 * matrix_scale; + if (!is_symmetric) { + return SolverFailure( + "solver-matrix-not-symmetric", + std::to_string(row) + ":" + std::to_string(column), + "The full public CSR values violate the approved symmetry " + "tolerance."); + } + } + } + + equation_count_ = static_cast(matrix.Rows()); + row_offsets_.clear(); + column_indices_.clear(); + values_.clear(); + row_offsets_.reserve(matrix.Rows() + 1U); + row_offsets_.push_back(0); + + for (std::size_t row = 0U; row < matrix.Rows(); ++row) { + bool has_diagonal = false; + for (std::size_t position = public_offsets[row]; + position < public_offsets[row + 1U]; ++position) { + const std::size_t column = public_columns[position]; + if (column < row) { + continue; + } + if (!ConvertsToMklInt(column) || + !ConvertsToMklInt(column_indices_.size())) { + return SolverFailure( + "solver-dimension-overflow", + std::to_string(row) + ":" + std::to_string(column), + "CSR indices exceed the oneMKL integer range."); + } + has_diagonal = has_diagonal || column == row; + column_indices_.push_back(static_cast(column)); + values_.push_back(public_values[position]); + } + if (!has_diagonal) { + return SolverFailure( + "solver-missing-diagonal", std::to_string(row), + "Every PARDISO SPD row must retain its diagonal slot."); + } + if (!ConvertsToMklInt(column_indices_.size())) { + return SolverFailure( + "solver-dimension-overflow", std::to_string(row), + "CSR row offsets exceed the oneMKL integer range."); + } + row_offsets_.push_back(static_cast(column_indices_.size())); + } + return Status::Ok(); +} + +Status PardisoSession::CopyValidatedFullCsr(const SparseMatrix& matrix) { + equation_count_ = static_cast(matrix.Rows()); + row_offsets_.clear(); + column_indices_.clear(); + values_.clear(); + row_offsets_.reserve(matrix.RowOffsets().size()); + column_indices_.reserve(matrix.ColumnIndices().size()); + values_.reserve(matrix.Values().size()); + + for (const std::size_t offset : matrix.RowOffsets()) { + if (!ConvertsToMklInt(offset)) { + return SolverFailure("solver-dimension-overflow", "row-offsets", + "CSR row offsets exceed the oneMKL integer " + "range."); + } + row_offsets_.push_back(static_cast(offset)); + } + for (const std::size_t column : matrix.ColumnIndices()) { + if (!ConvertsToMklInt(column)) { + return SolverFailure("solver-dimension-overflow", "column-indices", + "CSR indices exceed the oneMKL integer range."); + } + column_indices_.push_back(static_cast(column)); + } + values_.resize(matrix.Values().size()); + dense_blas_internal::CopyValues(matrix.Values().data(), values_.size(), + values_.data()); + return Status::Ok(); +} + +void PardisoSession::CallPardiso(const MKL_INT phase, double* rhs, + double* solution, MKL_INT& error) { + const dense_blas_internal::ScopedSingleEquationThreads thread_scope{ + static_cast(equation_count_)}; + pardiso(pt_.data(), &max_factorizations_, &matrix_number_, &mtype_, &phase, + &equation_count_, values_.data(), row_offsets_.data(), + column_indices_.data(), permutation_.data(), &rhs_count_, + iparm_.data(), &message_level_, rhs, solution, &error); + if (g_pardiso_backend_test_hook != nullptr) { + const internal::PardisoBackendTestCall call{ + phase, mtype_, equation_count_, row_offsets_.data(), + column_indices_.data(), values_.data(), solution}; + (*g_pardiso_backend_test_hook)(call, error, iparm_[13]); + } +} + +MKL_INT PardisoSession::Release() noexcept { + MKL_INT error = 0; + if (owns_pardiso_state_) { + const MKL_INT phase = -1; + double placeholder = 0.0; + CallPardiso(phase, &placeholder, &placeholder, error); + } + owns_pardiso_state_ = false; + factorized_ = false; + pt_.fill(nullptr); + iparm_.fill(0); + permutation_.clear(); + ClearOwnedArrays(); + return error; +} + +void PardisoSession::ClearOwnedArrays() noexcept { + equation_count_ = 0; + row_offsets_.clear(); + column_indices_.clear(); + values_.clear(); +} + +} // namespace pardiso_internal +} // namespace fesa diff --git a/src/fesa/solvers/linear/pardiso_session.h b/src/fesa/solvers/linear/pardiso_session.h new file mode 100644 index 0000000..28311c8 --- /dev/null +++ b/src/fesa/solvers/linear/pardiso_session.h @@ -0,0 +1,60 @@ +#ifndef FESA_SRC_FESA_SOLVERS_LINEAR_PARDISO_SESSION_H_ +#define FESA_SRC_FESA_SOLVERS_LINEAR_PARDISO_SESSION_H_ + +#include +#include + +#include + +#include "fesa/core/status.h" + +namespace fesa { +class SparseMatrix; +class Vector; + +namespace pardiso_internal { + +/// @brief Retains the two existing CSR and pivot policies. +enum class PardisoMatrixPolicy { kSpdUpper, kUnsymmetricFull }; + +/// @brief Owns one backend factorization, CSR storage and phase lifecycle. +/// @note No symbolic reuse is performed across Factorize calls. +class PardisoSession final { + public: + explicit PardisoSession(PardisoMatrixPolicy policy); + ~PardisoSession(); + PardisoSession(const PardisoSession&) = delete; + PardisoSession& operator=(const PardisoSession&) = delete; + + Status Factorize(const SparseMatrix& matrix); + Status Solve(const Vector& rhs, Vector& solution); + + private: + Status CopyValidatedUpperTriangle(const SparseMatrix& matrix); + Status CopyValidatedFullCsr(const SparseMatrix& matrix); + void CallPardiso(MKL_INT phase, double* rhs, double* solution, + MKL_INT& error); + MKL_INT Release() noexcept; + void ClearOwnedArrays() noexcept; + + std::array pt_{}; + std::array iparm_{}; + std::vector row_offsets_; + std::vector column_indices_; + std::vector permutation_; + std::vector values_; + MKL_INT equation_count_{0}; + MKL_INT max_factorizations_{1}; + MKL_INT matrix_number_{1}; + const PardisoMatrixPolicy policy_; + MKL_INT mtype_; + MKL_INT rhs_count_{1}; + MKL_INT message_level_{0}; + bool owns_pardiso_state_{false}; + bool factorized_{false}; +}; + +} // namespace pardiso_internal +} // namespace fesa + +#endif // FESA_SRC_FESA_SOLVERS_LINEAR_PARDISO_SESSION_H_ diff --git a/src/fesa/solvers/linear/pardiso_session_test_seam.h b/src/fesa/solvers/linear/pardiso_session_test_seam.h new file mode 100644 index 0000000..9cc0f02 --- /dev/null +++ b/src/fesa/solvers/linear/pardiso_session_test_seam.h @@ -0,0 +1,41 @@ +#ifndef FESA_SRC_FESA_SOLVERS_LINEAR_PARDISO_SESSION_TEST_SEAM_H_ +#define FESA_SRC_FESA_SOLVERS_LINEAR_PARDISO_SESSION_TEST_SEAM_H_ + +#include + +#include + +namespace fesa::internal { + +/// @brief Borrows call storage only while a backend observation hook runs. +struct PardisoBackendTestCall { + MKL_INT phase; + MKL_INT matrix_type; + MKL_INT equation_count; + const MKL_INT* row_offsets; + const MKL_INT* column_indices; + const double* values; + double* solution; +}; + +// Private fault seam runs after the real call, including real resource release. +// It is thread-local and never replaces a backend allocation with fake state. +using PardisoBackendTestHook = std::function; + +class ScopedPardisoBackendTestHook final { + public: + explicit ScopedPardisoBackendTestHook(PardisoBackendTestHook hook); + ~ScopedPardisoBackendTestHook(); + ScopedPardisoBackendTestHook(const ScopedPardisoBackendTestHook&) = delete; + ScopedPardisoBackendTestHook& operator=(const ScopedPardisoBackendTestHook&) = + delete; + + private: + PardisoBackendTestHook hook_; + PardisoBackendTestHook* previous_; +}; + +} // namespace fesa::internal + +#endif // FESA_SRC_FESA_SOLVERS_LINEAR_PARDISO_SESSION_TEST_SEAM_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index be0178c..5845708 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,7 @@ add_executable( unit/core/diagnostic_test.cpp unit/core/source_identity_test.cpp unit/core/status_test.cpp + unit/elements/element_capabilities_test.cpp unit/elements/element_factory_test.cpp unit/elements/hex8_element_test.cpp unit/elements/hex8_high_low_test.cpp diff --git a/tests/integration/analysis/nonlinear_static_procedure_test.cpp b/tests/integration/analysis/nonlinear_static_procedure_test.cpp index b5adfa5..8f41c34 100644 --- a/tests/integration/analysis/nonlinear_static_procedure_test.cpp +++ b/tests/integration/analysis/nonlinear_static_procedure_test.cpp @@ -223,16 +223,15 @@ std::vector ReadConstraintMask(const Hdf5File& file) { fesa::Vector3 SpatialMoment(const fesa::Vector3& phi, const fesa::Vector3& generalized) { - const fesa::Matrix transform = fesa::InverseTransposeLeftJacobianSO3(phi); - return {transform(0U, 0U) * generalized.X() + - transform(0U, 1U) * generalized.Y() + - transform(0U, 2U) * generalized.Z(), - transform(1U, 0U) * generalized.X() + - transform(1U, 1U) * generalized.Y() + - transform(1U, 2U) * generalized.Z(), - transform(2U, 0U) * generalized.X() + - transform(2U, 1U) * generalized.Y() + - transform(2U, 2U) * generalized.Z()}; + // Independent closed-form action, used only as a test oracle. + const double angle = phi.Norm(); + const double square = angle * angle; + const double coefficient = angle < 1e-4 ? + 1.0 / 12.0 + square / 720.0 + square * square / 30240.0 + + square * square * square / 1209600.0 : + (1.0 - 0.5 * angle / std::tan(0.5 * angle)) / square; + return generalized + 0.5 * phi.Cross(generalized) + + coefficient * phi.Cross(phi.Cross(generalized)); } std::array ReconstructGlobalBalance(const Hdf5File& file) { diff --git a/tests/integration/analysis/thermal_physics_test.cpp b/tests/integration/analysis/thermal_physics_test.cpp index de76b29..172628d 100644 --- a/tests/integration/analysis/thermal_physics_test.cpp +++ b/tests/integration/analysis/thermal_physics_test.cpp @@ -19,6 +19,8 @@ #include "thermal_gauss_oracle.h" #include "thermal_slab_oracle.h" +#include "fesa/assembly/thermal_assembler.h" + namespace { namespace ti = fesa::thermal_transient_internal; using fesa::Vector; diff --git a/tests/integration/app/thermal_application_test.cpp b/tests/integration/app/thermal_application_test.cpp index b97b05a..c43fd97 100644 --- a/tests/integration/app/thermal_application_test.cpp +++ b/tests/integration/app/thermal_application_test.cpp @@ -29,6 +29,8 @@ #include "fesa/solvers/linear/mkl_pardiso_solver.h" #include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h" +#include "fesa/analysis/transient_heat_procedure.h" + namespace { class HdfHandle { diff --git a/tests/reference/eigen_reference_cases_test.cpp b/tests/reference/eigen_reference_cases_test.cpp index 97aee97..d6c5af6 100644 --- a/tests/reference/eigen_reference_cases_test.cpp +++ b/tests/reference/eigen_reference_cases_test.cpp @@ -32,7 +32,16 @@ std::string ReadBytes(const std::filesystem::path& path) { TEST(EigenReference, RunsOriginalInputAndComparesAllModalFrameIdentities) { const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR}; const std::filesystem::path reference_directory = - source_root / "reference" / "shellNL-eigen-meshfine"; + std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / + "eigen-execution"; + std::filesystem::create_directories(reference_directory); + for (const auto& name : {"shellNL-eigen-meshfine.inp", + "shellNL-eigen-meshfine eigenvalues.txt"}) { + std::filesystem::copy_file( + source_root / "reference" / "shellNL-eigen-meshfine" / name, + reference_directory / name, + std::filesystem::copy_options::overwrite_existing); + } const std::filesystem::path input = reference_directory / "shellNL-eigen-meshfine.inp"; const std::filesystem::path text = diff --git a/tests/reference/nonlinear_static_reference_comparison.cpp b/tests/reference/nonlinear_static_reference_comparison.cpp index f23d8a2..874e701 100644 --- a/tests/reference/nonlinear_static_reference_comparison.cpp +++ b/tests/reference/nonlinear_static_reference_comparison.cpp @@ -1193,16 +1193,15 @@ fesa::Vector3 VectorAt(const std::vector& values, fesa::Vector3 SpatialMoment(const fesa::Vector3& phi, const fesa::Vector3& generalized) { - const Matrix transform = InverseTransposeLeftJacobianSO3(phi); - return {transform(0U, 0U) * generalized.X() + - transform(0U, 1U) * generalized.Y() + - transform(0U, 2U) * generalized.Z(), - transform(1U, 0U) * generalized.X() + - transform(1U, 1U) * generalized.Y() + - transform(1U, 2U) * generalized.Z(), - transform(2U, 0U) * generalized.X() + - transform(2U, 1U) * generalized.Y() + - transform(2U, 2U) * generalized.Z()}; + // Independent closed-form action, used only as a test oracle. + const double angle = phi.Norm(); + const double square = angle * angle; + const double coefficient = angle < 1e-4 ? + 1.0 / 12.0 + square / 720.0 + square * square / 30240.0 + + square * square * square / 1209600.0 : + (1.0 - 0.5 * angle / std::tan(0.5 * angle)) / square; + return generalized + 0.5 * phi.Cross(generalized) + + coefficient * phi.Cross(phi.Cross(generalized)); } fesa::Vector3 MaskedResidual(const Hdf5Projection& projection, diff --git a/tests/reference/thermal_reference_cases_test.cpp b/tests/reference/thermal_reference_cases_test.cpp index 1484203..8ff59e0 100644 --- a/tests/reference/thermal_reference_cases_test.cpp +++ b/tests/reference/thermal_reference_cases_test.cpp @@ -15,7 +15,14 @@ std::string Bytes(const std::filesystem::path& path) { } TEST(ThermalReference, OriginalAbaqusNt11RemainsAnExplicitFailingDiagnostic) { const auto folder = - std::filesystem::path{FESA_TEST_SOURCE_DIR} / "reference/shell-thermal"; + std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference/thermal-execution"; + std::filesystem::create_directories(folder); + for (const auto& name : {"shell-thermal.inp", + "shell-thermal NodalTemperature.csv"}) { + std::filesystem::copy_file( + std::filesystem::path{FESA_TEST_SOURCE_DIR} / "reference/shell-thermal" / name, + folder / name, std::filesystem::copy_options::overwrite_existing); + } const auto input = folder / "shell-thermal.inp"; const auto csv = folder / "shell-thermal NodalTemperature.csv"; const auto before_input = Bytes(input), before_csv = Bytes(csv); diff --git a/tests/test_compare_hex8.py b/tests/test_compare_hex8.py new file mode 100644 index 0000000..4311886 --- /dev/null +++ b/tests/test_compare_hex8.py @@ -0,0 +1,856 @@ +"""Independent comparison contracts; generated result fixtures are not source evidence.""" + +import csv +import importlib.util +import json +import math +import os +from pathlib import Path +import shutil +import subprocess +import sys + +import h5py +import numpy as np +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "compare_hex8", ROOT / "scripts/verification/compare_hex8.py" +) +COMPARE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(COMPARE) + + +def family(actual, reference): + identities = [{"node_label": i + 1, "component": "U1"} for i in range(len(actual))] + return COMPARE.evaluate_family( + {"case": "hexa_linearstatic", "step": "Step-1", "increment": 10, + "quantity": "U", "unit_dimensions": "length", "rows": identities}, + actual, reference, + ) + + +def test_zero_reference_scale_requires_exact_zero(): + report = family([0.0, 1e-300], [0.0, 0.0]) + assert not report["passed"] + assert report["diagnostic_code"] == "zero-reference-scale-nonzero-error" + + +def test_row_pass_does_not_override_one_percent_rms(): + report = family([1.02] * 10, [1.0] * 10) + assert report["rows_passed"] + assert not report["rms_passed"] + assert not report["passed"] + + +def test_near_zero_band_uses_reference_scale_only(): + report = family([100.0, 1.0], [100.0, 0.0]) + assert report["passed"] + assert report["rows"][1]["branch"] == "near-zero" + assert not family([100.0, 1.0001], [100.0, 0.0])["passed"] + + +def test_nonfinite_family_fails_before_tolerance(): + with pytest.raises(COMPARE.ComparisonError, match="finite"): + family([float("nan")], [1.0]) + + +def test_header_strip_collision_rejected_before_bad_numeric(tmp_path): + path = tmp_path / "collision.csv" + path.write_text("Node Label, Node Label\n1,NaN\n", encoding="utf-8") + with pytest.raises(COMPARE.ComparisonError) as error: + COMPARE.read_reference(path, "hexa_linearstatic", "U", {}) + assert error.value.code == "duplicate-reference-header" + + +def test_modal_quantity_and_units_are_explicit(): + rows = [{"step": "Step-1", "mode": i, "quantity": "eigenvalue", + "unit_dimensions": "1/time^2", "value": float(i)} for i in range(1, 11)] + with pytest.raises(COMPARE.ComparisonError) as error: + COMPARE.compare_modal(rows, rows, "frequency", "1/time") + assert error.value.code == "wrong-modal-quantity" + + +def test_modal_singletons_use_common_one_percent_rms(): + reference = [{"step": "Step-1", "mode": i, "quantity": "eigenvalue", + "unit_dimensions": "1/time^2", "value": float(i)} for i in range(1, 11)] + actual = [dict(row, value=row["value"] * 1.02) for row in reference] + report = COMPARE.compare_modal(reference, actual, "eigenvalue", "1/time^2") + assert not report["passed"] + assert len(report["families"]) == 10 + assert all(row["rows_passed"] and not row["rms_passed"] for row in report["families"]) + +# These schema declarations are independent of the comparator's declarations. +TEXT = h5py.string_dtype(encoding="utf-8") +REFERENCE = ROOT / "reference/hexa" + + +def write_strings(obj, attributes): + for name, value in attributes.items(): + obj.attrs.create(name, value, dtype=TEXT) + + +def compound(group, name, schema, rows): + data = np.empty(len(rows), dtype=np.dtype(schema)) + for i, row in enumerate(rows): + data[i] = row + return group.create_dataset(name, data=data) + + +@pytest.fixture(scope="module") +def source_models(): + return {case: COMPARE.input_inventory(REFERENCE / f"{case}.inp", case) + for case in ("hexa_linearstatic", "hexa_nonlinearstatic", "hexa_dynlinear", "hexa_dynnonlinear", "hexa_freq")} + + +def source_rows(case, quantity): + with (REFERENCE / f"{case}_{quantity}.csv").open(newline="", encoding="utf-8") as stream: + reader = csv.reader(stream) + headers = [value.strip() for value in next(reader)] + return [dict(zip(headers, [value.strip() for value in row])) for row in reader] + + +def make_result(path, model): + """Build an original-sized schema fixture, not a validated physical solution.""" + case = model["case"] + dynamic = case in ("hexa_dynlinear", "hexa_dynnonlinear") + nonlinear = case in ("hexa_nonlinearstatic", "hexa_dynnonlinear") + frequency = case == "hexa_freq" + reference_values = {} + if not frequency: + for quantity in (["U"] if dynamic else ["U", "RF", "S"]): + table_rows = {} + for row in source_rows(case, quantity): + increment = int(row["Frame"].split(":")[0].split()[-1]) + entity = int(row["Element Label" if quantity == "S" else "Node Label"]) + point = int(row["IntPt"]) if quantity == "S" else 0 + names = ("S11", "S22", "S33", "S12", "S13", "S23") if quantity == "S" else tuple(f"{quantity}{c}" for c in (1, 2, 3)) + table_rows[increment, entity, point] = [float(row[f"{quantity}-{name}"]) for name in names] + reference_values[quantity] = table_rows + with h5py.File(path, "w") as file: + metadata = file.create_group("metadata") + metadata.attrs.create("schema_version", np.uint64(1), dtype="f8", "wrong-result-value-type")]) +def test_wrong_shape_or_dtype_is_not_sliced_into_supported_components(tmp_path, result_fixtures, shape, dtype, code): + path = copy_result(tmp_path, result_fixtures) + with h5py.File(path, "r+") as file: + key = "steps/Step-1/frames/0/nodal/displacement" + del file[key] + file.create_dataset(key, shape=shape, dtype=dtype) + assert compare_fixture(path)["precheck"]["code"] == code + + +def test_signed_ids_and_fixed_ascii_are_not_official_compound_schema(tmp_path, result_fixtures): + path = copy_result(tmp_path, result_fixtures) + with h5py.File(path, "r+") as file: + old = file["model/nodes"][:] + del file["model/nodes"] + compound(file["model"], "nodes", [("internal_node_id", " #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/fem/dof_manager.h" + namespace { template diff --git a/tests/unit/analysis/analysis_test.cpp b/tests/unit/analysis/analysis_test.cpp index 72f5bf8..f45aa50 100644 --- a/tests/unit/analysis/analysis_test.cpp +++ b/tests/unit/analysis/analysis_test.cpp @@ -1,9 +1,20 @@ #include "fesa/analysis/analysis.h" +#include "fesa/analysis/analysis_procedure.h" +#include "fesa/analysis/prepared_analysis.h" +#include "analysis/equilibrium_algorithm.h" +#include "analysis/hht_reduced_integrator.h" +#include "analysis/hht_reduced_procedure.h" +#include "analysis/solid_hht_integrator.h" +#include "analysis/solid_hht_core.h" +#include "analysis/thermal_transient_algebra.h" +#include "fesa/solvers/linear/tangent_solver.h" #include #include #include +#include +#include #include #include @@ -65,3 +76,555 @@ TEST(Analysis, PropagatesStatusAndDestroysThroughMinimalAnalysis) { analysis.reset(); EXPECT_TRUE(destroyed); } + +namespace { + +using ProcedureSignature = fesa::Result ( + fesa::AnalysisProcedure::*)(const fesa::Domain &); +static_assert(std::has_virtual_destructor_v); +static_assert(std::is_abstract_v); +static_assert(std::is_same_v); + +class ObservedProcedure final : public fesa::AnalysisProcedure { +public: + ObservedProcedure(const fesa::Domain *&observed, bool &destroyed) + : observed_(observed), destroyed_(destroyed) {} + ~ObservedProcedure() override { destroyed_ = true; } + fesa::Result + Execute(const fesa::Domain &domain) override { + observed_ = &domain; + return fesa::Result::Failure( + fesa::Status::Failure(fesa::FailureCategory::kSolver, + {{fesa::Severity::kError, + "procedure-original-failure", + {}, + "ORIGINAL", + "source-identity", + "Original failure message."}})); + } + +private: + const fesa::Domain *&observed_; + bool &destroyed_; +}; + +} // namespace + +TEST(AnalysisProcedure, PreparedConsumerPreservesDomainFailureAndLifetime) { + const auto domain_result = fesa::Domain::Create({}); + ASSERT_TRUE(domain_result.HasValue()); + const auto &domain = domain_result.Value(); + const fesa::Domain *observed = nullptr; + bool destroyed = false; + std::unique_ptr procedure = + std::make_unique(observed, destroyed); + auto outcome = fesa::ExecutePreparedAnalysis(domain, *procedure); + EXPECT_EQ(observed, &domain); + ASSERT_FALSE(outcome.HasValue()); + EXPECT_EQ(outcome.GetStatus().Category(), fesa::FailureCategory::kSolver); + ASSERT_EQ(outcome.GetStatus().Diagnostics().size(), 1U); + const auto &diagnostic = outcome.GetStatus().Diagnostics()[0]; + EXPECT_EQ(diagnostic.code, "procedure-original-failure"); + EXPECT_EQ(diagnostic.keyword, "ORIGINAL"); + EXPECT_EQ(diagnostic.entity_identity, "source-identity"); + EXPECT_EQ(diagnostic.message, "Original failure message."); + EXPECT_FALSE(destroyed); + procedure.reset(); + EXPECT_TRUE(destroyed); +} + +TEST(EquilibriumAlgorithm, RetainsMoveOnlyCandidateUntilProcedureTakesIt) { + static_assert(std::has_virtual_destructor_v); + static_assert(std::is_abstract_v); + using Candidate = std::unique_ptr; + using EvaluationResult = fesa::Result>; + std::vector observed; + fesa::EquilibriumStep step( + [&](std::uint32_t iteration) { + observed.push_back(iteration); + if (iteration == 3U) + return EvaluationResult::Success(std::make_unique(42)); + return EvaluationResult::Success(std::nullopt); + }, 100U); + fesa::NewtonStepAlgorithm algorithm; + fesa::EquilibriumAlgorithm& base = algorithm; + EXPECT_FALSE(step.HasCandidate()); + ASSERT_TRUE(step.Solve(base).IsOk()); + EXPECT_EQ(observed, (std::vector{1U, 2U, 3U})); + ASSERT_TRUE(step.HasCandidate()); + Candidate accepted = step.TakeCandidate(); + ASSERT_NE(accepted, nullptr); + EXPECT_EQ(*accepted, 42); +} + +TEST(EquilibriumAlgorithm, FirstFailedEvaluationLeavesNoAcceptedCandidate) { + using EvaluationResult = fesa::Result>; + std::vector observed; + fesa::EquilibriumStep step( + [&](std::uint32_t iteration) { + observed.push_back(iteration); + return EvaluationResult::Failure(fesa::Status::Failure( + fesa::FailureCategory::kSolver, + {{fesa::Severity::kError, "first-evaluation-failure", {}, + "STATIC", "original-owner", "Original evaluation failure."}})); + }, 100U); + fesa::NewtonStepAlgorithm algorithm; + fesa::EquilibriumAlgorithm& base = algorithm; + const auto status = step.Solve(base); + EXPECT_EQ(observed, (std::vector{1U})); + EXPECT_FALSE(step.HasCandidate()); + ASSERT_FALSE(status.IsOk()); + ASSERT_EQ(status.Diagnostics().size(), 1U); + EXPECT_EQ(status.Diagnostics()[0].code, "first-evaluation-failure"); + EXPECT_EQ(status.Diagnostics()[0].entity_identity, "original-owner"); + EXPECT_EQ(status.Diagnostics()[0].message, "Original evaluation failure."); +} + +namespace { +// These helpers route supplied values only. They never call a concrete time +// integrator, Domain/assembly/time-grid builder, analysis or numerical solver. +fesa::Status InjectionFailure() { + return fesa::Status::Failure(fesa::FailureCategory::kSolver, + {{fesa::Severity::kError, "injected-policy-failure", {}, "MOCK", + "caller-owned", "Original injected failure."}}); +} +void ExpectInjectionFailure(const fesa::Status& status) { + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kSolver); + ASSERT_EQ(status.Diagnostics().size(), 1U); + const auto& diagnostic = status.Diagnostics()[0]; + EXPECT_EQ(diagnostic.code, "injected-policy-failure"); + EXPECT_EQ(diagnostic.keyword, "MOCK"); + EXPECT_EQ(diagnostic.entity_identity, "caller-owned"); + EXPECT_EQ(diagnostic.message, "Original injected failure."); +} +void ExpectValueVector(const fesa::Vector& value, double expected) { + ASSERT_EQ(value.Size(), 2U); + for (std::size_t i = 0; i < value.Size(); ++i) + EXPECT_DOUBLE_EQ(value[i], expected); +} +class NeverCalledLinear final : public fesa::LinearSolver { + public: + fesa::Status Factorize(const fesa::SparseMatrix&) override { + ADD_FAILURE() << "Mock dispatch must not factorize."; + return InjectionFailure(); + } + fesa::Status Solve(const fesa::Vector&, fesa::Vector&) const override { + ADD_FAILURE() << "Mock dispatch must not solve."; + return InjectionFailure(); + } +}; +class NeverCalledTangent final : public fesa::TangentSolver { + public: + fesa::Status FactorizeFresh(const fesa::SparseMatrix&) override { + ADD_FAILURE() << "Mock dispatch must not factorize."; + return InjectionFailure(); + } + fesa::Status SolveCorrection(const fesa::Vector&, fesa::Vector&) const override { + ADD_FAILURE() << "Mock dispatch must not solve."; + return InjectionFailure(); + } +}; +class SelectedOrderAlgorithm final : public fesa::EquilibriumAlgorithm { + public: + explicit SelectedOrderAlgorithm(bool& destroyed) : destroyed_(destroyed) {} + ~SelectedOrderAlgorithm() override { destroyed_ = true; } + bool fail = false; + fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last, + const Evaluation& evaluate) override { + EXPECT_EQ(first, 0U); + EXPECT_EQ(last, 3U); + if (fail) return InjectionFailure(); + auto first_result = evaluate(first); + if (!first_result.HasValue()) return first_result.GetStatus(); + EXPECT_FALSE(first_result.Value()); + auto last_result = evaluate(last); + if (!last_result.HasValue()) return last_result.GetStatus(); + EXPECT_TRUE(last_result.Value()); + return fesa::Status::Ok(); + } + private: + bool& destroyed_; +}; +namespace shell_value = fesa::hht_reduced_internal; +class ValueShellIntegrator final : public shell_value::ShellHhtIntegrator { + public: + ValueShellIntegrator(bool& destroyed, std::vector& events) + : destroyed_(destroyed), events_(events) {} + ~ValueShellIntegrator() override { destroyed_ = true; } + std::function predict; + std::function rates; + std::function( + const shell_value::FixedChart&, const shell_value::EmbeddingRoot&, + const fesa::Vector&, const fesa::Vector&, fesa::TangentSolver&)> reconstruct; + shell_value::ReducedShellPredictor Predict( + const shell_value::ReducedHistory& history, double h, + const fesa::HhtParameters& parameters) const override { + events_.push_back("predict"); + return predict(history, h, parameters); + } + void EvaluateRates(const shell_value::ReducedShellRateRequest& request, + fesa::Vector& velocity, fesa::Vector& acceleration) const override { + events_.push_back("rates"); + rates(request, velocity, acceleration); + } + fesa::Result ReconstructPhysicalRates( + const shell_value::FixedChart& chart, const shell_value::EmbeddingRoot& root, + const fesa::Vector& velocity, const fesa::Vector& acceleration, + fesa::TangentSolver& solver) const override { + events_.push_back("reconstruct"); + return reconstruct(chart, root, velocity, acceleration, solver); + } + private: + bool& destroyed_; + std::vector& events_; +}; +class ValueSolidIntegrator final : public fesa::detail::SolidHhtIntegrator { + public: + ValueSolidIntegrator(bool& destroyed, std::vector& events) + : destroyed_(destroyed), events_(events) {} + ~ValueSolidIntegrator() override { destroyed_ = true; } + std::function( + const fesa::SolidDynamicAcceptedFrameState&, double, + const fesa::HhtParameters&)> predict; + std::function( + const fesa::Vector&, const fesa::Vector&, + const fesa::detail::SolidHhtPairedPredictor&, const fesa::Vector&, + const fesa::Vector&, double, const fesa::HhtParameters&)> rates; + fesa::Result Predict( + const fesa::SolidDynamicAcceptedFrameState& old, double h, + const fesa::HhtParameters& parameters) const override { + events_.push_back("predict"); + return predict(old, h, parameters); + } + fesa::Result EvaluateRates( + const fesa::Vector& high, const fesa::Vector& low, + const fesa::detail::SolidHhtPairedPredictor& predictor, + const fesa::Vector& old_acceleration, + const fesa::Vector& old_acceleration_low, double h, + const fesa::HhtParameters& parameters) const override { + events_.push_back("rates"); + return rates(high, low, predictor, old_acceleration, old_acceleration_low, + h, parameters); + } + private: + bool& destroyed_; + std::vector& events_; +}; +namespace heat_value = fesa::thermal_transient_internal; +class ValueHeatIntegrator final : public heat_value::HeatStepIntegrator { + public: + ValueHeatIntegrator(bool& destroyed, std::vector& events) + : destroyed_(destroyed), events_(events) {} + ~ValueHeatIntegrator() override { destroyed_ = true; } + std::function( + const heat_value::ThermalSystem&, const heat_value::ThermalPartition&, + const fesa::Vector&, double, fesa::LinearSolver&, + heat_value::ThermalFactorization&)> solve; + fesa::Result SolveIncrement( + const heat_value::ThermalSystem& system, + const heat_value::ThermalPartition& partition, + const fesa::Vector& old, double delta, fesa::LinearSolver& solver, + heat_value::ThermalFactorization& cache) const override { + events_.push_back("solve"); + return solve(system, partition, old, delta, solver, cache); + } + private: + bool& destroyed_; + std::vector& events_; +}; +} // namespace + +TEST(EquilibriumAlgorithm, InjectsAlternativeOrderAndPreservesTypedPendingOnFailure) { + bool destroyed = false; + auto owned = std::make_unique(destroyed); + auto* selected = owned.get(); + std::unique_ptr policy = std::move(owned); + std::vector observed; + using Candidate = std::unique_ptr; + using EvaluationResult = fesa::Result>; + fesa::EquilibriumStep step( + [&](std::uint32_t iteration) { + observed.push_back(iteration); + return iteration == 3U + ? EvaluationResult::Success(std::make_unique(42)) + : EvaluationResult::Success(std::nullopt); + }, 3U); + ASSERT_TRUE(step.Solve(*policy).IsOk()); + EXPECT_EQ(observed, (std::vector{0U, 3U})); + selected->fail = true; + ExpectInjectionFailure(step.Solve(*policy)); + EXPECT_EQ(observed, (std::vector{0U, 3U})); + ASSERT_TRUE(step.HasCandidate()); + EXPECT_EQ(*step.TakeCandidate(), 42); + EXPECT_FALSE(destroyed); + policy.reset(); + EXPECT_TRUE(destroyed); + // The default algorithm still evaluates zero through the inclusive maximum. + std::vector zero_observed; + fesa::NewtonStepAlgorithm zero_algorithm; + fesa::EquilibriumStep zero_step([&](std::uint32_t iteration) { + zero_observed.push_back(iteration); + return iteration == 2U ? fesa::Result>::Success(7) + : fesa::Result>::Success(std::nullopt); + }, 2U); + ASSERT_TRUE(zero_step.Solve(zero_algorithm).IsOk()); + EXPECT_EQ(zero_observed, (std::vector{0U, 1U, 2U})); + ASSERT_TRUE(zero_step.HasCandidate()); + EXPECT_EQ(zero_step.TakeCandidate(), 7); +} + +TEST(ProcedureIntegratorInjection, ShellValuesUseProductionStepAndPreserveHistory) { + bool destroyed = false; + std::vector events; + auto owned = std::make_unique(destroyed, events); + auto* fake = owned.get(); + std::unique_ptr integrator = std::move(owned); + shell_value::ReducedHistory old{fesa::Vector(2, 1), fesa::Vector(2, 2), + fesa::Vector(2, 3), fesa::Vector(2, 4), fesa::Vector(2, 5)}; + shell_value::FixedChart chart; + shell_value::EmbeddingRoot root; + fesa::Vector algorithmic(2, 6), retained_v(2, 7), retained_a(2, 8); + const fesa::HhtParameters parameters{11, 12, 13}; + NeverCalledTangent solver; + bool fail_reconstruction = false, fail_commit = false; + fake->predict = [&](const auto& history, double h, const auto& p) { + EXPECT_EQ(&history, &old); EXPECT_EQ(&p, ¶meters); EXPECT_DOUBLE_EQ(h, 17); + return shell_value::ReducedShellPredictor{fesa::Vector(2, 21), fesa::Vector(2, 22), + fesa::Vector(2, 23), fesa::Vector(2, 24), fesa::Vector(2, 25), fesa::Vector(2, 26)}; + }; + fake->rates = [&](const auto& request, auto& velocity, auto& acceleration) { + EXPECT_EQ(&request.history, &old); EXPECT_EQ(&request.chart, &chart); + EXPECT_EQ(&request.trial_algorithmic, &algorithmic); + EXPECT_EQ(&request.retained_velocity, &retained_v); + EXPECT_EQ(&request.retained_acceleration, &retained_a); + EXPECT_EQ(&request.parameters, ¶meters); EXPECT_DOUBLE_EQ(request.h, 17); + EXPECT_FALSE(request.copy_translation_rates); ExpectValueVector(request.vpp, 22); + EXPECT_NE(velocity.Data(), acceleration.Data()); + EXPECT_NE(velocity.Data(), request.vpp.Data()); + EXPECT_NE(acceleration.Data(), request.trial_algorithmic.Data()); + ExpectValueVector(velocity, 0); ExpectValueVector(acceleration, 0); + for (std::size_t i = 0; i < 2; ++i) { velocity[i] = 31; acceleration[i] = 32; } + }; + fake->reconstruct = [&](const auto& c, const auto& r, const auto& v, + const auto& a, auto& backend) { + EXPECT_EQ(&c, &chart); EXPECT_EQ(&r, &root); EXPECT_EQ(&backend, &solver); + ExpectValueVector(v, 31); ExpectValueVector(a, 32); + if (fail_reconstruction) + return fesa::Result::Failure(InjectionFailure()); + return fesa::Result::Success({ + fesa::Vector(2, 41), fesa::Vector(2, 42), fesa::Vector(2, 43), fesa::Vector(2, 44)}); + }; + std::vector history; + const auto evaluate = [&](const auto& selected, auto predictor) { + ExpectValueVector(predictor.pp, 21); ExpectValueVector(predictor.vpp, 22); + ExpectValueVector(predictor.coordinate_low, 23); + ExpectValueVector(predictor.increment_trace, 24); + ExpectValueVector(predictor.increment_trace_compensation, 25); + ExpectValueVector(predictor.increment_trace_absolute_sum, 26); + shell_value::ReducedShellTrialRates rates{fesa::Vector(2), fesa::Vector(2)}; + auto* v_data = rates.velocity.Data(); auto* a_data = rates.acceleration.Data(); + selected.EvaluateRates({chart, old, algorithmic, predictor.vpp, false, + retained_v, retained_a, 17, parameters}, rates.velocity, rates.acceleration); + EXPECT_EQ(v_data, rates.velocity.Data()); EXPECT_EQ(a_data, rates.acceleration.Data()); + return selected.ReconstructPhysicalRates(chart, root, rates.velocity, + rates.acceleration, solver); + }; + const auto commit = [&](shell_value::ReconstructedRates&& pending) { + events.push_back("commit"); + ExpectValueVector(pending.velocity, 41); ExpectValueVector(pending.acceleration, 42); + ExpectValueVector(pending.z_velocity, 43); ExpectValueVector(pending.z_acceleration, 44); + if (fail_commit) return InjectionFailure(); + history.push_back(std::move(pending)); + return fesa::Status::Ok(); + }; + ASSERT_TRUE(shell_value::RunReducedHhtStep(*integrator, old, 17, parameters, + evaluate, commit).IsOk()); + EXPECT_EQ(events, (std::vector{"predict", "rates", "reconstruct", "commit"})); + ASSERT_EQ(history.size(), 1U); + events.clear(); fail_reconstruction = true; + ExpectInjectionFailure(shell_value::RunReducedHhtStep(*integrator, old, 17, + parameters, evaluate, commit)); + EXPECT_EQ(events, (std::vector{"predict", "rates", "reconstruct"})); + EXPECT_EQ(history.size(), 1U); + events.clear(); fail_reconstruction = false; fail_commit = true; + ExpectInjectionFailure(shell_value::RunReducedHhtStep(*integrator, old, 17, + parameters, evaluate, commit)); + EXPECT_EQ(events, (std::vector{"predict", "rates", "reconstruct", "commit"})); + EXPECT_EQ(history.size(), 1U); + ExpectValueVector(old.p, 1); ExpectValueVector(old.velocity, 2); + ExpectValueVector(old.acceleration, 3); ExpectValueVector(old.algorithmic_acceleration, 4); + ExpectValueVector(old.z, 5); ExpectValueVector(history[0].velocity, 41); + EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed); +} + +TEST(ProcedureIntegratorInjection, SolidPairedValuesUseProductionStepAndPreserveHistory) { + bool destroyed = false; + std::vector events; + auto owned = std::make_unique(destroyed, events); + auto* fake = owned.get(); + std::unique_ptr integrator = std::move(owned); + fesa::SolidDynamicAcceptedFrameState old; + old.displacement = fesa::Vector(2, 1); old.low.displacement = fesa::Vector(2, 2); + old.velocity = fesa::Vector(2, 3); old.low.velocity = fesa::Vector(2, 4); + old.acceleration = fesa::Vector(2, 5); old.low.acceleration = fesa::Vector(2, 6); + old.algorithmic_acceleration = fesa::Vector(2, 7); + old.low.algorithmic_acceleration = fesa::Vector(2, 8); + const fesa::HhtParameters parameters{11, 12, 13}; + bool fail_predict = false, fail_rates = false, fail_commit = false; + fake->predict = [&](const auto& frame, double h, const auto& p) { + EXPECT_EQ(&frame, &old); EXPECT_EQ(&p, ¶meters); EXPECT_DOUBLE_EQ(h, 17); + if (fail_predict) + return fesa::Result::Failure(InjectionFailure()); + return fesa::Result::Success({ + fesa::Vector(2, 21), fesa::Vector(2, 22), fesa::Vector(2, 23), fesa::Vector(2, 24)}); + }; + fake->rates = [&](const auto& high, const auto& low, const auto& predictor, + const auto& old_a, const auto& old_a_low, double h, const auto& p) { + ExpectValueVector(high, 1); ExpectValueVector(low, 2); + ExpectValueVector(predictor.displacement, 21); ExpectValueVector(predictor.displacement_low, 22); + ExpectValueVector(predictor.velocity, 23); ExpectValueVector(predictor.velocity_low, 24); + EXPECT_EQ(&old_a, &old.acceleration); EXPECT_EQ(&old_a_low, &old.low.acceleration); + EXPECT_EQ(&p, ¶meters); EXPECT_DOUBLE_EQ(h, 17); + if (fail_rates) + return fesa::Result::Failure(InjectionFailure()); + return fesa::Result::Success({ + fesa::Vector(2, 31), fesa::Vector(2, 32), fesa::Vector(2, 33), + fesa::Vector(2, 34), fesa::Vector(2, 35), fesa::Vector(2, 36)}); + }; + std::vector history; + const auto failure = [&](const fesa::Status& status) { + events.push_back("predict-failure"); return status; + }; + const auto evaluate = [&](const auto& selected, const auto& predictor) { + auto pending = old; + const auto status = fesa::detail::SetSolidHhtRates(selected, old, predictor, + 17, parameters, &pending); + if (!status.IsOk()) { + ExpectValueVector(pending.velocity, 3); ExpectValueVector(pending.low.velocity, 4); + ExpectValueVector(pending.acceleration, 5); ExpectValueVector(pending.low.acceleration, 6); + ExpectValueVector(pending.algorithmic_acceleration, 7); + ExpectValueVector(pending.low.algorithmic_acceleration, 8); + return fesa::Result::Failure(status); + } + return fesa::Result::Success(std::move(pending)); + }; + const auto commit = [&](fesa::SolidDynamicAcceptedFrameState&& pending) { + events.push_back("commit"); + ExpectValueVector(pending.displacement, 1); ExpectValueVector(pending.low.displacement, 2); + ExpectValueVector(pending.velocity, 31); ExpectValueVector(pending.low.velocity, 32); + ExpectValueVector(pending.acceleration, 33); ExpectValueVector(pending.low.acceleration, 34); + ExpectValueVector(pending.algorithmic_acceleration, 35); + ExpectValueVector(pending.low.algorithmic_acceleration, 36); + if (fail_commit) return InjectionFailure(); + history.push_back(std::move(pending)); return fesa::Status::Ok(); + }; + ASSERT_TRUE(fesa::detail::RunSolidHhtStep(*integrator, old, 17, parameters, + failure, evaluate, commit).IsOk()); + EXPECT_EQ(events, (std::vector{"predict", "rates", "commit"})); + ASSERT_EQ(history.size(), 1U); + events.clear(); fail_predict = true; + ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17, + parameters, failure, evaluate, commit)); + EXPECT_EQ(events, (std::vector{"predict", "predict-failure"})); + EXPECT_EQ(history.size(), 1U); + events.clear(); fail_predict = false; fail_rates = true; + ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17, + parameters, failure, evaluate, commit)); + EXPECT_EQ(events, (std::vector{"predict", "rates"})); + EXPECT_EQ(history.size(), 1U); + events.clear(); fail_rates = false; fail_commit = true; + ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17, + parameters, failure, evaluate, commit)); + EXPECT_EQ(events, (std::vector{"predict", "rates", "commit"})); + EXPECT_EQ(history.size(), 1U); + ExpectValueVector(old.displacement, 1); ExpectValueVector(old.low.displacement, 2); + ExpectValueVector(old.velocity, 3); ExpectValueVector(old.low.velocity, 4); + ExpectValueVector(old.acceleration, 5); ExpectValueVector(old.low.acceleration, 6); + ExpectValueVector(old.algorithmic_acceleration, 7); ExpectValueVector(old.low.algorithmic_acceleration, 8); + ExpectValueVector(history[0].low.algorithmic_acceleration, 36); + EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed); +} + +TEST(ProcedureIntegratorInjection, HeatValuesUseProductionRecoveryCommitAndPreserveHistory) { + bool destroyed = false; + std::vector events; + auto owned = std::make_unique(destroyed, events); + auto* fake = owned.get(); + std::unique_ptr integrator = std::move(owned); + auto matrix = fesa::SparseMatrix::FromCoo( + 2, 2, {}, fesa::SparsePattern{{0, 0, 0}, {}}); + ASSERT_TRUE(matrix.HasValue()); + const auto& k = matrix.Value(); + const fesa::SparseMatrix c{k}; + const fesa::Vector q(2, 2), old(2, 1); + const heat_value::ThermalSystem system{k, c, q}; + const heat_value::ThermalPartition partition{{0, 1}, {}, fesa::Vector(0)}; + heat_value::ThermalFactorization cache; + NeverCalledLinear solver; + bool fail_solve = false, fail_recovery = false, fail_commit = false; + fake->solve = [&](const auto& supplied, const auto& p, const auto& previous, + double delta, auto& backend, auto& workspace) { + EXPECT_EQ(&supplied, &system); EXPECT_EQ(&p, &partition); EXPECT_EQ(&previous, &old); + EXPECT_EQ(&backend, &solver); EXPECT_EQ(&workspace, &cache); EXPECT_DOUBLE_EQ(delta, 17); + EXPECT_EQ(&supplied.k, &k); EXPECT_EQ(&supplied.c, &c); EXPECT_EQ(&supplied.q, &q); + EXPECT_EQ(supplied.k_error, nullptr); EXPECT_EQ(supplied.c_error, nullptr); + EXPECT_EQ(supplied.q_error, nullptr); EXPECT_EQ(p.free, (std::vector{0, 1})); + EXPECT_TRUE(p.constrained.empty()); EXPECT_EQ(p.prescribed.Size(), 0U); + if (fail_solve) + return fesa::Result::Failure(InjectionFailure()); + heat_value::ThermalStepCandidate pending{fesa::Vector(2, 21), fesa::Vector(2, 22), + fesa::Vector(2, 23), fesa::Vector(2, 24), + {31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, true, 42, 43}}; + return fesa::Result::Success(std::move(pending)); + }; + const heat_value::ThermalRecovery recovery = [&](const fesa::Vector& temperature) { + events.push_back("recovery"); ExpectValueVector(temperature, 21); + if (fail_recovery) + return fesa::Result>::Failure(InjectionFailure()); + std::vector flux(1); + for (auto& value : flux[0]) value = fesa::Vector3{51, 52, 53}; + return fesa::Result>::Success(std::move(flux)); + }; + std::vector history; + const auto commit = [&](heat_value::ThermalStepCandidate&& pending, + std::vector&& flux) { + events.push_back("commit"); + ExpectValueVector(pending.temperature, 21); ExpectValueVector(pending.residual, 22); + ExpectValueVector(pending.reaction, 23); ExpectValueVector(pending.certified_residual, 24); + const auto& e = pending.evidence; + EXPECT_DOUBLE_EQ(e.beta_cert, 31); EXPECT_DOUBLE_EQ(e.beta_limit, 32); + EXPECT_DOUBLE_EQ(e.free_residual_cert, 33); EXPECT_DOUBLE_EQ(e.full_free_residual_cert, 34); + EXPECT_DOUBLE_EQ(e.full_beta_cert, 35); EXPECT_DOUBLE_EQ(e.operator_construction_error, 36); + EXPECT_DOUBLE_EQ(e.rhs_construction_error, 37); EXPECT_DOUBLE_EQ(e.construction_identity_bound, 38); + EXPECT_DOUBLE_EQ(e.energy_change, 39); EXPECT_DOUBLE_EQ(e.energy_defect, 40); + EXPECT_DOUBLE_EQ(e.energy_bound, 41); EXPECT_TRUE(e.dissipation_checked); + EXPECT_DOUBLE_EQ(e.dissipation_defect, 42); EXPECT_DOUBLE_EQ(e.dissipation_bound, 43); + EXPECT_EQ(flux.size(), 1U); + for (const auto& value : flux[0]) EXPECT_EQ(value.Components(), (std::array{51, 52, 53})); + if (fail_commit) return InjectionFailure(); + history.push_back(std::move(pending)); return fesa::Status::Ok(); + }; + ASSERT_TRUE(heat_value::RunThermalStep(*integrator, system, partition, old, + 17, solver, cache, recovery, commit).IsOk()); + EXPECT_EQ(events, (std::vector{"solve", "recovery", "commit"})); + ASSERT_EQ(history.size(), 1U); + events.clear(); fail_solve = true; + ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition, + old, 17, solver, cache, recovery, commit)); + EXPECT_EQ(events, (std::vector{"solve"})); EXPECT_EQ(history.size(), 1U); + events.clear(); fail_solve = false; fail_recovery = true; + ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition, + old, 17, solver, cache, recovery, commit)); + EXPECT_EQ(events, (std::vector{"solve", "recovery"})); EXPECT_EQ(history.size(), 1U); + events.clear(); fail_recovery = false; fail_commit = true; + ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition, + old, 17, solver, cache, recovery, commit)); + EXPECT_EQ(events, (std::vector{"solve", "recovery", "commit"})); + EXPECT_EQ(history.size(), 1U); ExpectValueVector(old, 1); + ExpectValueVector(history[0].temperature, 21); EXPECT_FALSE(cache.matrix.has_value()); + EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed); +} diff --git a/tests/unit/analysis/eigen_reduction_test.cpp b/tests/unit/analysis/eigen_reduction_test.cpp index a7f9108..4d4ba8a 100644 --- a/tests/unit/analysis/eigen_reduction_test.cpp +++ b/tests/unit/analysis/eigen_reduction_test.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -93,38 +92,6 @@ TEST(EigenReduction, RemovesOnlyActualFreeDirectorNull) { .HasValue()); } -TEST(EigenReduction, RejectsNonfiniteUsedBasisAndPreservesPriorCandidate) { - const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}}; - const auto mass = NodalMass(frame.director); - fesa::Matrix stiffness(6U, 6U); - for (std::size_t i = 0; i < 5U; ++i) stiffness(i, i) = mass(i, i) * 7.0; - stiffness(5U, 5U) = .25; - const auto matrix = Sparse(stiffness); - const std::vector mask(6U, 0U); - const auto prior = fesa::eigen_internal::BuildEigenReduction( - matrix, {mass}, {frame}, mask, {.25}, 2.0); - ASSERT_TRUE(prior.HasValue()); - const auto prior_values = prior.Value().whitened_operator.Values(); - for (const double invalid : {std::numeric_limits::quiet_NaN(), - std::numeric_limits::infinity()}) { - for (const bool first_tangent : {false, true}) { - auto bad_frame = frame; - (first_tangent ? bad_frame.tangent_a : bad_frame.tangent_b)[0] = invalid; - const auto rejected = fesa::eigen_internal::BuildEigenReduction( - matrix, {mass}, {bad_frame}, mask, {.25}, 2.0); - ASSERT_FALSE(rejected.HasValue()); - EXPECT_EQ(rejected.GetStatus().Category(), fesa::FailureCategory::kSolver); - EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].code, - "invalid-eigen-mass-space"); - EXPECT_EQ(prior.Value().whitened_operator.Values(), prior_values); - } - } - const auto rerun = fesa::eigen_internal::BuildEigenReduction( - matrix, {mass}, {frame}, mask, {.25}, 2.0); - ASSERT_TRUE(rerun.HasValue()); - EXPECT_EQ(rerun.Value().whitened_operator.Values(), prior_values); -} - TEST(EigenReduction, RejectsPositiveButUnresolvableMassWithoutDeletingDofs) { const double p = 1e-8; const std::array d{2 * p / (1 + p * p), 0, @@ -215,3 +182,70 @@ TEST(EigenReduction, HandlesTwoFreeRotationsAndRejectsEmptyFiniteSpace) { EXPECT_EQ(empty.GetStatus().Diagnostics()[0].code, "empty-eigenproblem"); } } + +TEST(EigenReduction, PureMassWhiteningRetainsInclusiveInfinityConditionGate) { + const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}}; + for (const double diagonal : {0x1p-31, 0x1p-32, 0x1p-33}) { + auto mass = NodalMass(frame.director); + mass(0, 0) = 1.0; + mass(1, 1) = diagonal; + fesa::Matrix stiffness{6U, 6U}; + stiffness(0, 0) = 3.0; + stiffness(1, 1) = 3.0 * diagonal; + const auto reduced = fesa::eigen_internal::BuildEigenReduction( + Sparse(stiffness), {mass}, {frame}, {0, 0, 1, 1, 1, 1}, {0.25}, 1.0); + EXPECT_EQ(reduced.HasValue(), diagonal >= 0x1p-32); + if (reduced.HasValue()) { + EXPECT_DOUBLE_EQ(reduced.Value().nodes[0].record.rcond_inf, diagonal); + fesa::Vector one{2U, 1.0}; + const auto applied = reduced.Value().whitened_operator.Multiply(one); + EXPECT_NEAR(applied[0], 3.0, 1e-12); + EXPECT_NEAR(applied[1], 3.0, 1e-12); + } else { + EXPECT_EQ(reduced.GetStatus().Diagnostics()[0].code, + "eigen-mass-conditioning"); + } + } +} + +TEST(EigenReduction, PureOffDiagonalWhiteningMatchesAnalyticGramAndPencil) { + const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}}; + auto mass = NodalMass(frame.director); + mass(0, 1) = mass(1, 0) = 1.0; + fesa::Matrix stiffness{6U, 6U}; + stiffness(0, 0) = stiffness(1, 1) = 6.0; + stiffness(0, 1) = stiffness(1, 0) = 4.0; + // For M=[[2,1],[1,2]], physical whitening columns are analytically + // (1/sqrt(2),0) and (-1/sqrt(6),2/sqrt(6)). No eigensolver is invoked. + for (const double length : {0.25, 3.0, 16.0}) { + const auto reduced = fesa::eigen_internal::BuildEigenReduction( + Sparse(stiffness), {mass}, {frame}, {0, 0, 1, 1, 1, 1}, {0.25}, length); + ASSERT_TRUE(reduced.HasValue()); + fesa::Matrix canonical{2U, 2U}; + canonical(0, 0) = canonical(1, 1) = 1.0; + std::array physical{fesa::Vector{6U}, fesa::Vector{6U}}; + for (std::size_t column = 0; column < 2U; ++column) { + physical[column] = fesa::eigen_internal::RestoreScaledMode( + reduced.Value(), canonical, column); + for (std::size_t i = 0; i < 3U; ++i) physical[column][i] *= length; + } + EXPECT_NEAR(physical[0][0], 1.0 / std::sqrt(2.0), 1e-12); + EXPECT_NEAR(physical[0][1], 0.0, 1e-12); + EXPECT_NEAR(physical[1][0], -1.0 / std::sqrt(6.0), 1e-12); + EXPECT_NEAR(physical[1][1], 2.0 / std::sqrt(6.0), 1e-12); + const double expected[2][2]{{3.0, 1.0 / std::sqrt(3.0)}, + {1.0 / std::sqrt(3.0), 7.0 / 3.0}}; + for (std::size_t column = 0; column < 2U; ++column) { + fesa::Vector coordinate{2U}; + coordinate[column] = 1.0; + const auto action = reduced.Value().whitened_operator.Multiply(coordinate); + const auto mq = mass.Multiply(physical[column]); + const auto kq = stiffness.Multiply(physical[column]); + for (std::size_t row = 0; row < 2U; ++row) { + EXPECT_NEAR(physical[row].Dot(mq), row == column ? 1.0 : 0.0, 1e-12); + EXPECT_NEAR(action[row], expected[row][column], 1e-12); + EXPECT_NEAR(action[row], physical[row].Dot(kq), 1e-12); + } + } + } +} diff --git a/tests/unit/analysis/hht_reduced_shell_test.cpp b/tests/unit/analysis/hht_reduced_shell_test.cpp index d8ce519..1933119 100644 --- a/tests/unit/analysis/hht_reduced_shell_test.cpp +++ b/tests/unit/analysis/hht_reduced_shell_test.cpp @@ -5,8 +5,6 @@ #include #include #include -#include -#include #include #include #include @@ -21,7 +19,6 @@ #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" @@ -29,71 +26,6 @@ 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; @@ -462,480 +394,6 @@ 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, @@ -1683,58 +1141,4 @@ TEST(HhtReducedChannelNewton, RestrictedSingularityIsNotFullInvertibility) { EXPECT_FALSE(solver.FactorizeFresh(restricted.Value().tangent).IsOk()); } -TEST(HhtReducedOperators, NullMetricKeepsFiniteLeftAssociatedProducts) { - 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, false); - ASSERT_TRUE(assembled.HasValue()); - auto assembly = assembled.Value(); - const double maximum = (std::numeric_limits::max)(); - const double epsilon = std::ldexp(1.0, -26); - const double t = std::sqrt(1.0 - epsilon * epsilon); - const std::array n{epsilon, 0.6 * t, 0.8 * t}; - const std::array block{0.0, maximum, maximum, - maximum, 0.0, 0.0, - maximum, 0.0, 0.0}; - double original_metric = 0.0; - for (std::size_t i = 0U; i < 3U; ++i) - for (std::size_t j = 0U; j < 3U; ++j) - original_metric += (n[i] * block[3U * i + j]) * n[j]; - ASSERT_TRUE(std::isfinite(original_metric)); - ASSERT_GT(original_metric, 0.0); - ASSERT_FALSE(std::isfinite(block[1U] * n[1U] + block[2U] * n[2U])); - std::vector entries; - for (std::size_t node = 0U; node < assembly.rotations.size(); ++node) { - assembly.rotations[node].drill_null = fesa::Vector3(n); - for (std::size_t i = 0U; i < 3U; ++i) - for (std::size_t j = 0U; j < 3U; ++j) - entries.push_back({6U * node + 3U + i, 6U * node + 3U + j, - block[3U * i + j], node, 3U * i + j}); - } - const auto metric = fesa::hht_internal::MakeSparse( - fixture.dofs.FullDofCount(), fixture.dofs.FullDofCount(), - std::move(entries)); - ASSERT_TRUE(metric.HasValue()); - assembly.drilling_metric = metric.Value(); - const auto chart = fesa::hht_reduced_internal::BuildFixedChart( - fixture.dofs, assembly.rotations); - ASSERT_TRUE(chart.HasValue()); - fesa::hht_reduced_internal::ConstraintOperators operators; - operators.rows.resize(chart.Value().null_nodes.size()); - for (std::size_t row = 0U; row < operators.rows.size(); ++row) - for (std::size_t component = 0U; component < 3U; ++component) - operators.rows[row].gradient.push_back( - {6U * chart.Value().null_nodes[row] + 3U + component, - original_metric * n[component]}); - fesa::MklPardisoTangentSolver solver; - const auto checked = fesa::hht_reduced_internal::CheckGraphRegularity( - assembly, chart.Value(), operators, solver); - ASSERT_TRUE(checked.HasValue()); - EXPECT_GE(checked.Value(), std::ldexp(1.0, -32)); -} - } // namespace diff --git a/tests/unit/analysis/hht_shell_physics_test.cpp b/tests/unit/analysis/hht_shell_physics_test.cpp index e5a3548..13b49c9 100644 --- a/tests/unit/analysis/hht_shell_physics_test.cpp +++ b/tests/unit/analysis/hht_shell_physics_test.cpp @@ -573,8 +573,7 @@ TEST(HhtShellPhysics, ExternalDrillingHessianBalancesGradientAcceleration) { const fesa::Vector3 velocity{1.0, 0.0, 1.0}; const fesa::Vector3 acceleration{0.0, -1.0 / 3.0, 0.0}; const fesa::Vector3 force{1.0, 0.0, 0.0}; - const auto external = fixture.shells[0U].internal - .EvaluateExternalDrillingConstraint( + const auto external = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint( 0U, phi, velocity, force, {}, {}); ASSERT_TRUE(external.HasValue()); double gradient_acceleration = 0.0; @@ -586,14 +585,11 @@ TEST(HhtShellPhysics, ExternalDrillingHessianBalancesGradientAcceleration) { EXPECT_NEAR(external.Value().directional_second + gradient_acceleration, 0.0, 1.0e-14); constexpr double step = 1.0e-4; - const auto plus = fixture.shells[0U].internal - .EvaluateExternalDrillingConstraint( + const auto plus = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint( 0U, {step, 0.0, step}, {}, force, {}, {}); - const auto middle = fixture.shells[0U].internal - .EvaluateExternalDrillingConstraint( + const auto middle = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint( 0U, phi, {}, force, {}, {}); - const auto minus = fixture.shells[0U].internal - .EvaluateExternalDrillingConstraint( + const auto minus = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint( 0U, {-step, 0.0, -step}, {}, force, {}, {}); ASSERT_TRUE(plus.HasValue()); ASSERT_TRUE(middle.HasValue()); diff --git a/tests/unit/analysis/solid_hht_dynamic_procedure_test.cpp b/tests/unit/analysis/solid_hht_dynamic_procedure_test.cpp index f577af2..3eb945c 100644 --- a/tests/unit/analysis/solid_hht_dynamic_procedure_test.cpp +++ b/tests/unit/analysis/solid_hht_dynamic_procedure_test.cpp @@ -21,6 +21,8 @@ #include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h" #include "solid_hht_test_fixture.h" +#include "fesa/analysis/analysis_model.h" + namespace { fesa::Result RunDynamics( fesa::ModelDefinition d) { diff --git a/tests/unit/analysis/solid_static_procedure_test.cpp b/tests/unit/analysis/solid_static_procedure_test.cpp index acebcee..19e0fee 100644 --- a/tests/unit/analysis/solid_static_procedure_test.cpp +++ b/tests/unit/analysis/solid_static_procedure_test.cpp @@ -1,4 +1,7 @@ #include "fesa/analysis/solid_static_procedure.h" +#include "analysis/solid_static_core.h" +#include "analysis/equilibrium_algorithm.h" +#include "fesa/core/source_identity.h" #include @@ -7,6 +10,7 @@ #include #include #include +#include #include #include @@ -824,3 +828,116 @@ TEST(SolidStaticProcedure, << " energy=" << frame.physical_strain_energy << '\n'; } } + +namespace { +fesa::Status StaticPolicyFailure() { + return fesa::Status::Failure(fesa::FailureCategory::kSolver, + {{fesa::Severity::kError, "injected-static-algorithm", {}, "STATIC", + "original-owner", "Original algorithm failure."}}); +} +class UnusedStaticSolvers final : public fesa::LinearSolver, + public fesa::TangentSolver { + public: + mutable int calls = 0; + fesa::Status Factorize(const fesa::SparseMatrix&) override { + ++calls; return StaticPolicyFailure(); + } + fesa::Status Solve(const fesa::Vector&, fesa::Vector&) const override { + ++calls; return StaticPolicyFailure(); + } + fesa::Status FactorizeFresh(const fesa::SparseMatrix&) override { + ++calls; return StaticPolicyFailure(); + } + fesa::Status SolveCorrection(const fesa::Vector&, fesa::Vector&) const override { + ++calls; return StaticPolicyFailure(); + } +}; +class RefusingStaticAlgorithm final : public fesa::EquilibriumAlgorithm { + public: + RefusingStaticAlgorithm(const fesa::SolidStaticState& state, bool& destroyed) + : state_(state), destroyed_(destroyed) {} + ~RefusingStaticAlgorithm() override { destroyed_ = true; } + int calls = 0; + fesa::SolidStaticFrame baseline; + fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last, + const Evaluation&) override { + ++calls; + EXPECT_EQ(first, 1U); EXPECT_EQ(last, 100U); + EXPECT_TRUE(state_.AcceptedFrames().empty()); + EXPECT_TRUE(state_.IterationRecords().empty()); + baseline = state_.CommittedState(); + return StaticPolicyFailure(); + } + private: + const fesa::SolidStaticState& state_; + bool& destroyed_; +}; +} // namespace + +TEST(SolidStaticProcedure, InjectedAlgorithmFailurePreservesInitializedBaseline) { + auto domain = Map(Deck(Supports(), Tension(1.0), true)); + ASSERT_TRUE(domain.HasValue()) << Message(domain.GetStatus()); + fesa::SerialParallelFor serial; + UnusedStaticSolvers solvers; + fesa::SolidStaticState state; + bool destroyed = false; + auto owned = std::make_unique(state, destroyed); + auto* selected = owned.get(); + std::unique_ptr algorithm = std::move(owned); + { + fesa::SolidStaticCore core(serial, solvers, solvers, *algorithm); + const auto status = core.ExecuteInto(domain.Value(), state); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kSolver); + ASSERT_EQ(status.Diagnostics().size(), 1U); + const auto& diagnostic = status.Diagnostics()[0]; + EXPECT_EQ(diagnostic.code, "injected-static-algorithm"); + EXPECT_EQ(diagnostic.keyword, "STATIC"); + EXPECT_EQ(diagnostic.entity_identity, "original-owner"); + EXPECT_EQ(diagnostic.message, "Original algorithm failure."); + } + EXPECT_EQ(selected->calls, 1); EXPECT_EQ(solvers.calls, 0); + EXPECT_TRUE(state.AcceptedFrames().empty()); + EXPECT_TRUE(state.IterationRecords().empty()); + // Core initialization legitimately installs the zero-factor baseline before + // iteration. Compare to that exact baseline at algorithm entry, not to the + // initially uninitialized observer. + const auto& expected = selected->baseline; + const auto& actual = state.CommittedState(); + EXPECT_EQ(actual.frame_index, expected.frame_index); + EXPECT_EQ(actual.increment_index, expected.increment_index); + EXPECT_DOUBLE_EQ(actual.load_factor, expected.load_factor); + EXPECT_EQ(actual.iteration_count, expected.iteration_count); + EXPECT_EQ(actual.is_factor_one, expected.is_factor_one); + EXPECT_EQ(actual.nodal_internal_order, expected.nodal_internal_order); + ASSERT_EQ(actual.nodal_source_ids.size(), expected.nodal_source_ids.size()); + for (std::size_t i = 0; i < actual.nodal_source_ids.size(); ++i) + EXPECT_TRUE(fesa::SourceIdentityEquals(actual.nodal_source_ids[i], expected.nodal_source_ids[i])); + const auto same_vector = [](const fesa::Vector& a, const fesa::Vector& b) { + ASSERT_EQ(a.Size(), b.Size()); + for (std::size_t i = 0; i < a.Size(); ++i) EXPECT_DOUBLE_EQ(a[i], b[i]); + }; + same_vector(actual.displacement, expected.displacement); + same_vector(actual.internal_force, expected.internal_force); + same_vector(actual.external_force, expected.external_force); + same_vector(actual.residual, expected.residual); same_vector(actual.reaction, expected.reaction); + EXPECT_DOUBLE_EQ(actual.physical_strain_energy, expected.physical_strain_energy); + EXPECT_EQ(actual.global_equilibrium, expected.global_equilibrium); + EXPECT_EQ(actual.balance.internal_force_sum, expected.balance.internal_force_sum); + EXPECT_EQ(actual.balance.reference_internal_moment, expected.balance.reference_internal_moment); + EXPECT_EQ(actual.balance.current_internal_moment, expected.balance.current_internal_moment); + EXPECT_EQ(actual.balance.free_residual_moment, expected.balance.free_residual_moment); + EXPECT_EQ(actual.balance.linearization_moment, expected.balance.linearization_moment); + EXPECT_EQ(actual.balance.internal_force_scale, expected.balance.internal_force_scale); + EXPECT_EQ(actual.balance.reference_moment_scale, expected.balance.reference_moment_scale); + EXPECT_EQ(actual.balance.current_moment_scale, expected.balance.current_moment_scale); + ASSERT_EQ(actual.solid_results.size(), expected.solid_results.size()); + for (std::size_t i = 0; i < actual.solid_results.size(); ++i) { + const auto& a = actual.solid_results[i]; const auto& b = expected.solid_results[i]; + EXPECT_EQ(a.element, b.element); EXPECT_EQ(a.integration_point, b.integration_point); + EXPECT_EQ(a.natural_coordinates, b.natural_coordinates); + EXPECT_EQ(a.reference_coordinates, b.reference_coordinates); + EXPECT_EQ(a.cauchy_stress, b.cauchy_stress); EXPECT_EQ(a.strain, b.strain); + } + EXPECT_FALSE(destroyed); algorithm.reset(); EXPECT_TRUE(destroyed); +} diff --git a/tests/unit/analysis/thermal_analysis_state_test.cpp b/tests/unit/analysis/thermal_analysis_state_test.cpp index c1f58b8..04a092a 100644 --- a/tests/unit/analysis/thermal_analysis_state_test.cpp +++ b/tests/unit/analysis/thermal_analysis_state_test.cpp @@ -11,6 +11,8 @@ #include #include +#include "fesa/assembly/thermal_assembler.h" + namespace { fesa::ModelDefinition Definition(bool constrained = true, double period = .9, double increment = .3, diff --git a/tests/unit/analysis/transient_heat_procedure_test.cpp b/tests/unit/analysis/transient_heat_procedure_test.cpp index b31af42..646c0e3 100644 --- a/tests/unit/analysis/transient_heat_procedure_test.cpp +++ b/tests/unit/analysis/transient_heat_procedure_test.cpp @@ -12,6 +12,8 @@ #include "analysis/thermal_transient_bounds.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" +#include "fesa/assembly/thermal_assembler.h" + namespace { namespace ti = fesa::thermal_transient_internal; std::string Messages(const fesa::Status& status) { diff --git a/tests/unit/assembly/load_assembler_test.cpp b/tests/unit/assembly/load_assembler_test.cpp index e8f9b5a..b8f5ad8 100644 --- a/tests/unit/assembly/load_assembler_test.cpp +++ b/tests/unit/assembly/load_assembler_test.cpp @@ -481,47 +481,3 @@ TEST(LoadAssembly, ZeroLoadsRemainZero) { ASSERT_TRUE(constrained_rhs.HasValue()); EXPECT_EQ(constrained_rhs.Value().Size(), 0U); } - -TEST(LoadAssembly, PreservesOrderedCancellationInPrescribedCorrection) { - const std::filesystem::path source{"models/load-assembly.inp"}; - auto fixture = MakeFixture( - 1U, {}, - {{"10", 1, 1, 1.0, {source, 21U}}, - {"10", 3, 3, 1.0, {source, 22U}}, - {"10", 5, 5, 1.0, {source, 23U}}}, - {}); - const double large = 72057594037927936.0; // 2^56, exactly representable. - const auto kfc = MakeDenseSparse( - 3U, 3U, {large, 1.0, -large, large, -large, 1.0, 0.0, 0.0, 0.0}); - - const auto rhs = fesa::LoadAssembler::EffectiveFreeRhs( - fesa::Vector{6U}, kfc, fixture.dofs->PrescribedValues(), *fixture.dofs); - - ASSERT_TRUE(rhs.HasValue()); - ASSERT_EQ(rhs.Value().Size(), 3U); - EXPECT_DOUBLE_EQ(rhs.Value()[0U], 0.0); - EXPECT_DOUBLE_EQ(rhs.Value()[1U], -1.0); - EXPECT_DOUBLE_EQ(rhs.Value()[2U], 0.0); -} - -TEST(LoadAssembly, SelectsFirstOrderedOverflowDiagnostic) { - const std::filesystem::path source{"models/load-assembly.inp"}; - auto fixture = MakeFixture( - 1U, {}, - {{"10", 1, 1, 1.0, {source, 21U}}, - {"10", 3, 3, 1.0, {source, 22U}}, - {"10", 5, 5, 2.0, {source, 23U}}}, - {}); - const double maximum = (std::numeric_limits::max)(); - const auto kfc = MakeDenseSparse( - 3U, 3U, {maximum, maximum, 0.0, 0.0, 0.0, maximum, 0.0, 0.0, 0.0}); - - const auto rhs = fesa::LoadAssembler::EffectiveFreeRhs( - fesa::Vector{6U}, kfc, fixture.dofs->PrescribedValues(), *fixture.dofs); - - ExpectFailureCode(rhs, "nonfinite-load-accumulation"); - ASSERT_EQ(rhs.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ(rhs.GetStatus().Diagnostics()[0U].entity_identity, "0"); - EXPECT_NE(rhs.GetStatus().Diagnostics()[0U].message.find("row sum"), - std::string::npos); -} diff --git a/tests/unit/assembly/sparse_assembler_test.cpp b/tests/unit/assembly/sparse_assembler_test.cpp index 564ddbd..33ef767 100644 --- a/tests/unit/assembly/sparse_assembler_test.cpp +++ b/tests/unit/assembly/sparse_assembler_test.cpp @@ -185,7 +185,12 @@ class ReverseParallelFor final : public fesa::ParallelFor { class FakeStiffnessElement final : public fesa::Element { public: FakeStiffnessElement(fesa::ElementDofLayout layout, fesa::Matrix stiffness) - : layout_{std::move(layout)}, stiffness_{std::move(stiffness)} {} + : layout_{std::move(layout)}, stiffness_{std::move(stiffness)}, + contribution_layout_{layout_} {} + + void ReturnLayout(fesa::ElementDofLayout layout) { + contribution_layout_ = std::move(layout); + } const fesa::ElementDofLayout& DofLayout() const noexcept override { return layout_; @@ -194,7 +199,7 @@ class FakeStiffnessElement final : public fesa::Element { fesa::Result ComputeStiffness() const override { return fesa::Result::Success( - {layout_, stiffness_}); + {contribution_layout_, stiffness_}); } fesa::Result Recover( @@ -206,6 +211,7 @@ class FakeStiffnessElement final : public fesa::Element { private: fesa::ElementDofLayout layout_; fesa::Matrix stiffness_; + fesa::ElementDofLayout contribution_layout_; }; fesa::Matrix MatrixFromRows(const std::vector>& rows) { @@ -414,4 +420,59 @@ TEST(SparseAssembly, S4AndS4rSemanticFixturesAssembleIdenticalStiffness) { ExpectByteIdentical(s4r.Value(), s4.Value()); } +TEST(SparseAssembly, RejectsLayoutAndDimensionsInStableElementOrder) { + auto domain = fesa::Domain::Create(MakeDefinition()); + ASSERT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + ASSERT_TRUE(model.HasValue()); + const fesa::ElementDofLayout first_layout{ + {"Beam-1", 10, "10"}, {0U, 1U}, {fesa::DofComponent::kUx}}; + const fesa::ElementDofLayout second_layout{ + {"Beam-1", 20, "20"}, {1U, 2U}, {fesa::DofComponent::kUx}}; + FakeStiffnessElement first{first_layout, fesa::Matrix{2U, 2U}}; + FakeStiffnessElement second{second_layout, fesa::Matrix{1U, 1U}}; + const fesa::ElementView elements{std::cref(first), std::cref(second)}; + fesa::DofManager dofs; + ASSERT_TRUE(dofs.Build(model.Value(), elements).IsOk()); + const auto original_pattern = dofs.GetSparsePattern(); + + std::vector invalid(5U, first_layout); + invalid[0U].source_id.instance_name = "beam-1"; + invalid[1U].source_id.source_label = 11; + invalid[2U].source_id.source_label_text = "010"; + invalid[3U].node_indices = {1U, 0U}; + invalid[4U].components_per_node = {fesa::DofComponent::kUy}; + fesa::SerialParallelFor serial; + fesa::TbbParallelFor tbb; + ReverseParallelFor reverse; + for (const auto& layout : invalid) { + first.ReturnLayout(layout); + for (const fesa::ParallelFor* executor : + std::array{&serial, &tbb, &reverse}) { + auto candidate = + fesa::SparseAssembler::Assemble(elements, dofs, *executor); + ASSERT_FALSE(candidate.HasValue()); + EXPECT_EQ(candidate.GetStatus().Category(), fesa::FailureCategory::kModel); + ASSERT_EQ(candidate.GetStatus().Diagnostics().size(), 1U); + const auto& diagnostic = candidate.GetStatus().Diagnostics()[0U]; + EXPECT_EQ(diagnostic.code, "invalid-assembly-layout"); + EXPECT_EQ(diagnostic.keyword, "*ELEMENT"); + EXPECT_EQ(diagnostic.entity_identity, "10"); + EXPECT_EQ(diagnostic.message, + "Element stiffness layout must match its declared runtime layout."); + } + } + first.ReturnLayout(first_layout); + auto candidate = fesa::SparseAssembler::Assemble(elements, dofs, reverse); + ASSERT_FALSE(candidate.HasValue()); + EXPECT_EQ(candidate.GetStatus().Diagnostics()[0U].code, + "invalid-assembly-dimensions"); + EXPECT_EQ(candidate.GetStatus().Diagnostics()[0U].entity_identity, "20"); + EXPECT_EQ(first.DofLayout().node_indices, first_layout.node_indices); + EXPECT_EQ(dofs.GetSparsePattern().row_offsets, original_pattern.row_offsets); + EXPECT_EQ(dofs.GetSparsePattern().column_indices, + original_pattern.column_indices); + EXPECT_TRUE(dofs.ValidateInvariants().IsOk()); +} + } // namespace diff --git a/tests/unit/constraints/essential_constraints_test.cpp b/tests/unit/constraints/essential_constraints_test.cpp index 166f0d0..88ea888 100644 --- a/tests/unit/constraints/essential_constraints_test.cpp +++ b/tests/unit/constraints/essential_constraints_test.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -185,6 +186,24 @@ TEST(EssentialConstraints, HandlesNoAllAndMixedConstraints) { ExpectShape(all.Value().k_cc, 6U, 6U); EXPECT_EQ(all.Value().k_cc.Values(), full.Values()); + fesa::Vector full_load{6U}; + for (std::size_t i = 0U; i < full_load.Size(); ++i) + full_load[i] = static_cast(i) - 2.0; + auto empty_rhs = policy.EffectiveFreeRhs( + full_load, all.Value().k_fc, all_constraints.PrescribedValues(), + all_constraints); + ASSERT_TRUE(empty_rhs.HasValue()); + EXPECT_EQ(empty_rhs.Value().Size(), 0U); + auto full_rhs = policy.EffectiveFreeRhs( + full_load, none.Value().k_fc, no_constraints.PrescribedValues(), + no_constraints); + ASSERT_TRUE(full_rhs.HasValue()); + for (std::size_t i = 0U; i < full_load.Size(); ++i) { + EXPECT_EQ(full_rhs.Value()[i], static_cast(i) - 2.0); + EXPECT_EQ(full_load[i], static_cast(i) - 2.0); + } + + const auto mixed_constraints = MakeDofs({{"1", 3, 4, 0.0, {{}, 12U}}}); auto mixed = policy.Partition(full, mixed_constraints); ASSERT_TRUE(mixed.HasValue()); @@ -316,3 +335,52 @@ TEST(EssentialConstraints, RejectsDimensionOrOrderMismatch) { fesa::Vector{4U}, fesa::Vector{2U}, dofs)), std::invalid_argument); } + +TEST(EssentialConstraints, PreservesEliminationFailureOrderAndInputs) { + const fesa::EssentialConstraintPolicy policy; + const auto dofs = MakeDofs({{"1", 2, 2, 2.0, {{}, 12U}}, + {"1", 5, 5, -1.0, {{}, 13U}}}); + const double maximum = (std::numeric_limits::max)(); + fesa::Vector load{6U}; + load[0U] = std::numeric_limits::infinity(); + fesa::Vector prescribed{2U}; + prescribed[0U] = std::numeric_limits::infinity(); + prescribed[1U] = 1.0; + const auto kfc = MakeMatrix(4U, 2U, {0.4 * maximum, 0.8 * maximum, + maximum, 0.0, 0.0, 0.0, 0.0, 0.0}); + const auto original_values = kfc.Values(); + const auto original_pattern = dofs.GetSparsePattern(); + auto candidate = policy.EffectiveFreeRhs( + load, MakeMatrix(3U, 2U, std::vector(6U)), prescribed, dofs); + ASSERT_FALSE(candidate.HasValue()); + EXPECT_EQ(candidate.GetStatus().Diagnostics()[0U].code, + "invalid-load-dimensions"); + candidate = policy.EffectiveFreeRhs(load, kfc, prescribed, dofs); + ASSERT_FALSE(candidate.HasValue()); + EXPECT_EQ(candidate.GetStatus().Diagnostics()[0U].entity_identity, + "full-load:0"); + EXPECT_EQ(load[0U], std::numeric_limits::infinity()); + EXPECT_EQ(prescribed[0U], std::numeric_limits::infinity()); + + load[0U] = 7.0; + prescribed[0U] = 2.0; + candidate = policy.EffectiveFreeRhs(load, kfc, prescribed, dofs); + ASSERT_FALSE(candidate.HasValue()); + EXPECT_EQ(candidate.GetStatus().Category(), fesa::FailureCategory::kModel); + ASSERT_EQ(candidate.GetStatus().Diagnostics().size(), 1U); + const auto& diagnostic = candidate.GetStatus().Diagnostics()[0U]; + EXPECT_EQ(diagnostic.code, "nonfinite-load-accumulation"); + EXPECT_EQ(diagnostic.keyword, "LOAD_ASSEMBLER"); + EXPECT_EQ(diagnostic.entity_identity, "0"); + EXPECT_TRUE(diagnostic.location.file.empty()); + EXPECT_EQ(diagnostic.location.line, 0U); + EXPECT_EQ(diagnostic.message, + "Kfc times prescribed displacement produced a nonfinite row sum."); + EXPECT_EQ(load[0U], 7.0); + EXPECT_EQ(prescribed[0U], 2.0); + EXPECT_EQ(prescribed[1U], 1.0); + EXPECT_EQ(kfc.Values(), original_values); + EXPECT_EQ(dofs.GetSparsePattern().row_offsets, original_pattern.row_offsets); + EXPECT_EQ(dofs.GetSparsePattern().column_indices, + original_pattern.column_indices); +} diff --git a/tests/unit/core/ascii_test.cpp b/tests/unit/core/ascii_test.cpp index c0fff58..ec0036c 100644 --- a/tests/unit/core/ascii_test.cpp +++ b/tests/unit/core/ascii_test.cpp @@ -17,6 +17,21 @@ TEST(Ascii, LowercasesOnlyAsciiUppercaseBytes) { EXPECT_EQ(fesa::AsciiLower(non_ascii), non_ascii); } +TEST(Ascii, UppercaseCanonicalizationPreservesEveryOtherByteAndSourceText) { + std::string source; + std::string expected; + for (unsigned int byte = 0U; byte < 256U; ++byte) { + source.push_back(static_cast(byte)); + expected.push_back( + static_cast(byte >= 97U && byte <= 122U ? byte - 32U : byte)); + } + const std::string original = source; + EXPECT_EQ(fesa::AsciiUppercase(source), expected); + EXPECT_EQ(source, original); + EXPECT_TRUE(fesa::AsciiUppercase("").empty()); + EXPECT_EQ(fesa::AsciiUppercase("Shell-01"), "SHELL-01"); +} + TEST(Ascii, ComparesNamesWithoutLocaleOrUnicodeFolding) { EXPECT_TRUE(fesa::AsciiCaseInsensitiveEquals("Beam-1", "bEaM-1")); EXPECT_FALSE(fesa::AsciiCaseInsensitiveEquals("Beam-1", "Beam-10")); diff --git a/tests/unit/core/source_identity_test.cpp b/tests/unit/core/source_identity_test.cpp index 0201bf7..f0ac82a 100644 --- a/tests/unit/core/source_identity_test.cpp +++ b/tests/unit/core/source_identity_test.cpp @@ -17,4 +17,12 @@ TEST(CoreDiagnostics, SourceIdentityPreservesRawIdentity) { EXPECT_EQ(identity.instance_name, "Beam-Instance_A"); EXPECT_EQ(identity.source_label, 42); EXPECT_EQ(identity.source_label_text, "00042"); + + EXPECT_TRUE(fesa::SourceIdentityEquals(identity, identity)); + EXPECT_FALSE(fesa::SourceIdentityEquals( + identity, {"beam-instance_a", std::int64_t{42}, "00042"})); + EXPECT_FALSE(fesa::SourceIdentityEquals( + identity, {"Beam-Instance_A", std::int64_t{43}, "00042"})); + EXPECT_FALSE(fesa::SourceIdentityEquals( + identity, {"Beam-Instance_A", std::int64_t{42}, "42"})); } diff --git a/tests/unit/core/status_test.cpp b/tests/unit/core/status_test.cpp index 90ace79..cd3223f 100644 --- a/tests/unit/core/status_test.cpp +++ b/tests/unit/core/status_test.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -19,8 +20,68 @@ fesa::Diagnostic ModelDiagnostic() { "Beam length must be positive."}; } +struct CopyOnlyPayload { + explicit CopyOnlyPayload(int initial_value) : value{initial_value} {} + CopyOnlyPayload(const CopyOnlyPayload& other) + : value{other.value}, copies{other.copies + 1} {} + + int value; + int copies = 0; +}; + +struct TransferPayload { + explicit TransferPayload(int initial_value) : value{initial_value} {} + TransferPayload(const TransferPayload& other) + : value{other.value}, copies{other.copies + 1}, moves{other.moves} {} + TransferPayload(TransferPayload&& other) noexcept + : value{other.value}, copies{other.copies}, moves{other.moves + 1} { + other.value = -1; + } + + int value; + int copies = 0; + int moves = 0; +}; + } // namespace +TEST(CoreDiagnostics, ResultOwnsCopyOnlyPayloadWithoutIntermediateCopy) { + CopyOnlyPayload source{23}; + auto result = fesa::Result::Success(source); + + ASSERT_TRUE(result.HasValue()); + EXPECT_TRUE(result.GetStatus().IsOk()); + EXPECT_EQ(result.Value().value, 23); + EXPECT_EQ(result.Value().copies, 2); + result.Value().value = 42; + EXPECT_EQ(source.value, 23); + EXPECT_EQ(source.copies, 0); +} + +TEST(CoreDiagnostics, ResultTransfersOwnedPayloadDirectlyIntoValue) { + TransferPayload source{17}; + const auto from_lvalue = fesa::Result::Success(source); + EXPECT_EQ(source.value, 17); + EXPECT_EQ(source.copies, 0); + EXPECT_EQ(source.moves, 0); + EXPECT_EQ(from_lvalue.Value().value, 17); + EXPECT_EQ(from_lvalue.Value().copies, 1); + EXPECT_EQ(from_lvalue.Value().moves, 1); + + const auto from_temporary = + fesa::Result::Success(TransferPayload{29}); + EXPECT_EQ(from_temporary.Value().value, 29); + EXPECT_EQ(from_temporary.Value().copies, 0); + EXPECT_EQ(from_temporary.Value().moves, 1); + + auto owned = std::make_unique(31); + const auto move_only = + fesa::Result>::Success(std::move(owned)); + EXPECT_EQ(owned, nullptr); + ASSERT_NE(move_only.Value(), nullptr); + EXPECT_EQ(*move_only.Value(), 31); +} + TEST(CoreDiagnostics, ResultEnforcesValueErrorExclusivity) { const fesa::Status ok = fesa::Status::Ok(); EXPECT_TRUE(ok.IsOk()); diff --git a/tests/unit/elements/element_capabilities_test.cpp b/tests/unit/elements/element_capabilities_test.cpp new file mode 100644 index 0000000..585d749 --- /dev/null +++ b/tests/unit/elements/element_capabilities_test.cpp @@ -0,0 +1,413 @@ +#include "fesa/elements/element_capabilities.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "fesa/elements/element_factory.h" +#include "fesa/elements/hex8_element.h" +#include "fesa/elements/nonlinear_mitc4_inertia.h" +#include "fesa/elements/thermal_shell.h" + +namespace { + +void Same(double a, double b) { + EXPECT_EQ(std::memcmp(&a, &b, sizeof(double)), 0); +} +template +void Same(const std::array& a, const std::array& b) { + for (std::size_t i = 0; i < N; ++i) Same(a[i], b[i]); +} +void Same(const fesa::Vector& a, const fesa::Vector& b) { + ASSERT_EQ(a.Size(), b.Size()); + for (std::size_t i = 0; i < a.Size(); ++i) Same(a[i], b[i]); +} +void Same(const fesa::Matrix& a, const fesa::Matrix& b) { + ASSERT_EQ(a.Rows(), b.Rows()); + ASSERT_EQ(a.Columns(), b.Columns()); + for (std::size_t i = 0; i < a.Rows(); ++i) + for (std::size_t j = 0; j < a.Columns(); ++j) Same(a(i, j), b(i, j)); +} +void Same(const fesa::Vector3& a, const fesa::Vector3& b) { + for (std::size_t i = 0; i < 3; ++i) Same(a[i], b[i]); +} +void Same(const fesa::Status& a, const fesa::Status& b) { + EXPECT_EQ(a.Category(), b.Category()); + ASSERT_EQ(a.Diagnostics().size(), b.Diagnostics().size()); + for (std::size_t i = 0; i < a.Diagnostics().size(); ++i) { + const auto& x = a.Diagnostics()[i]; + const auto& y = b.Diagnostics()[i]; + EXPECT_EQ(x.code, y.code); + EXPECT_EQ(x.location.file, y.location.file); + EXPECT_EQ(x.location.line, y.location.line); + EXPECT_EQ(x.keyword, y.keyword); + EXPECT_EQ(x.entity_identity, y.entity_identity); + EXPECT_EQ(x.message, y.message); + } +} +fesa::ElementDofLayout ShellLayout() { + return {{"shell", 9, "0009"}, {7, 2, 9, 4}, + {fesa::DofComponent::kUx, fesa::DofComponent::kUy, + fesa::DofComponent::kUz, fesa::DofComponent::kUrx, + fesa::DofComponent::kUry, fesa::DofComponent::kUrz}}; +} +struct ShellInput { + std::array nodes{{ + {{"shell", 1, "1"}, {-1., -1., 0.}, {"cap.inp", 1}}, + {{"shell", 2, "2"}, {1., -1., 0.}, {"cap.inp", 2}}, + {{"shell", 3, "3"}, {1., 1., 0.}, {"cap.inp", 3}}, + {{"shell", 4, "4"}, {-1., 1., 0.}, {"cap.inp", 4}}}}; + std::array frames{{ + {0, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}}, + {1, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}}, + {2, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}}, + {3, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}}}}; + fesa::ShellSection section{"section", .2, 0, {"cap.inp", 20}}; + fesa::LinearElasticMaterial material{"elastic", 120., .25, {"cap.inp", 21}}; + std::array Pointers() const { + return {&nodes[0], &nodes[1], &nodes[2], &nodes[3]}; + } +}; +fesa::NonlinearMitc4TrialDofs Trial() { + fesa::NonlinearMitc4TrialDofs trial{}; + for (std::size_t i = 0; i < 4; ++i) { + const double n = static_cast(i + 1); + trial.displacement[i] = {.001 * n, -.002 * n, .0005 * n}; + trial.rotation_vector[i] = {.01 * n, -.005 * n, .002 * n}; + } + return trial; +} +void Same(const fesa::NonlinearMitc4Recovery& a, + const fesa::NonlinearMitc4Recovery& b) { + Same(a.physical_strain_energy, b.physical_strain_energy); + for (std::size_t g = 0; g < 4; ++g) { + const auto& x = a.gauss_points[g]; + const auto& y = b.gauss_points[g]; + EXPECT_EQ(x.location, y.location); + for (std::size_t i = 0; i < 2; ++i) + Same(x.natural_coordinates[i], y.natural_coordinates[i]); + for (std::size_t i = 0; i < 8; ++i) { + Same(x.generalized_strain[i], y.generalized_strain[i]); + Same(x.section_resultant[i], y.section_resultant[i]); + } + Same(x.local_frame.e1, y.local_frame.e1); + Same(x.local_frame.e2, y.local_frame.e2); + Same(x.local_frame.e3, y.local_frame.e3); + for (std::size_t s = 0; s < 3; ++s) + for (std::size_t i = 0; i < x.stress[s].components.size(); ++i) + Same(x.stress[s].components[i], y.stress[s].components[i]); + } +} + +TEST(ElementCapabilities, ShellTrialPreservesCompleteDirectionalResponseAndFailure) { + ShellInput input; + auto made = fesa::NonlinearMitc4Shell::Create( + input.Pointers(), input.frames, input.section, input.material); + ASSERT_TRUE(made.HasValue()); + auto kernel = std::make_unique(std::move(made.Value())); + std::unique_ptr capability = + std::make_unique(ShellLayout(), *kernel); + const auto trial = Trial(); + fesa::Vector direction{24}; + for (std::size_t i = 0; i < 24; ++i) direction[i] = .003 * (i + 1); + const auto saved_direction = direction; + const auto direct = fesa::EvaluateInternalResidualAndTangent(*kernel, trial, &direction); + const auto via = capability->EvaluateTrial(trial, &direction); + ASSERT_TRUE(direct.HasValue()); + ASSERT_TRUE(via.HasValue()); + const auto& a = direct.Value(); + const auto& b = via.Value(); + Same(a.internal_residual, b.internal_residual); + Same(a.internal_tangent, b.internal_tangent); + Same(a.internal_force, b.internal_force); + Same(a.recovery, b.recovery); + Same(a.physical_strain_energy, b.physical_strain_energy); + Same(a.physical_force, b.physical_force); + Same(a.drilling_force, b.drilling_force); + Same(a.drilling_tangent, b.drilling_tangent); + Same(a.drilling_metric, b.drilling_metric); + Same(a.drilling_energy, b.drilling_energy); + Same(a.force_directional_second, b.force_directional_second); + Same(a.drilling_constraint, b.drilling_constraint); + Same(a.drilling_constraint_gradient, b.drilling_constraint_gradient); + for (std::size_t n = 0; n < 4; ++n) { + Same(a.drilling_constraint_hessian[n], b.drilling_constraint_hessian[n]); + Same(a.drilling_constraint_third_directional_gradient[n], + b.drilling_constraint_third_directional_gradient[n]); + } + Same(direction, saved_direction); + EXPECT_EQ(capability->DofLayout().node_indices, ShellLayout().node_indices); + EXPECT_EQ(capability->DofLayout().components_per_node, ShellLayout().components_per_node); + auto invalid = trial; + invalid.rotation_vector[2] = {std::numeric_limits::quiet_NaN(), 0., 0.}; + const auto failed_direct = fesa::EvaluateInternalResidualAndTangent(*kernel, invalid); + const auto failed_via = capability->EvaluateTrial(invalid); + ASSERT_FALSE(failed_direct.HasValue()); + ASSERT_FALSE(failed_via.HasValue()); + Same(failed_direct.GetStatus(), failed_via.GetStatus()); + const auto again = capability->EvaluateTrial(trial, &direction); + ASSERT_TRUE(again.HasValue()); + Same(again.Value().internal_force, a.internal_force); +} + +TEST(ElementCapabilities, ShellExternalConstraintRetainsEveryVariation) { + ShellInput input; + const auto kernel = fesa::NonlinearMitc4Shell::Create( + input.Pointers(), input.frames, input.section, input.material); + ASSERT_TRUE(kernel.HasValue()); + fesa::Mitc4TrialAdapter adapter{ShellLayout(), kernel.Value()}; + const fesa::ShellTrialElement& capability = adapter; + const fesa::Vector3 phi{.12, -.23, .07}, rate{.31, .02, -.17}; + const fesa::Vector3 force{1., -2., 3.}, force_rate{.2, .7, -.4}, acceleration{.1, -.3, .9}; + const auto direct = kernel.Value().EvaluateExternalDrillingConstraint( + 2, phi, rate, force, force_rate, acceleration); + const auto via = capability.EvaluateExternalDrillingConstraint( + 2, phi, rate, force, force_rate, acceleration); + ASSERT_TRUE(direct.HasValue()); + ASSERT_TRUE(via.HasValue()); + const auto& a = direct.Value(); + const auto& b = via.Value(); + Same(a.value, b.value); + Same(a.gradient, b.gradient); + Same(a.hessian, b.hessian); + Same(a.directional_first, b.directional_first); + Same(a.directional_second, b.directional_second); + Same(a.gradient_directional_first, b.gradient_directional_first); + Same(a.gradient_directional_second, b.gradient_directional_second); + const auto failed_direct = kernel.Value().EvaluateExternalDrillingConstraint( + 4, phi, rate, force, force_rate, acceleration); + const auto failed_via = capability.EvaluateExternalDrillingConstraint( + 4, phi, rate, force, force_rate, acceleration); + ASSERT_FALSE(failed_direct.HasValue()); + ASSERT_FALSE(failed_via.HasValue()); + Same(failed_direct.GetStatus(), failed_via.GetStatus()); +} + +TEST(ElementCapabilities, ShellInertiaRetainsCompletePhysicalResponse) { + ShellInput input; + const auto kernel = fesa::NonlinearMitc4Inertia::Create( + input.Pointers(), input.frames, input.section, input.material, 2.); + ASSERT_TRUE(kernel.HasValue()); + std::unique_ptr capability = + std::make_unique(ShellLayout(), kernel.Value()); + const auto trial = Trial(); + fesa::Vector v{24}, acceleration{24}; + for (std::size_t i = 0; i < 24; ++i) { + v[i] = .013 * (i + 1); + acceleration[i] = -.007 * (i + 1); + } + const auto saved_v = v, saved_acceleration = acceleration; + const auto direct = kernel.Value().Evaluate(trial, v, acceleration); + const auto via = capability->Evaluate(trial, v, acceleration); + ASSERT_TRUE(direct.HasValue()); + ASSERT_TRUE(via.HasValue()); + const auto& a = direct.Value(); + const auto& b = via.Value(); + Same(a.mass, b.mass); + Same(a.position_tangent, b.position_tangent); + Same(a.velocity_tangent, b.velocity_tangent); + Same(a.force, b.force); + Same(a.convective_force, b.convective_force); + Same(a.kinetic_energy, b.kinetic_energy); + Same(a.momentum, b.momentum); + Same(a.angular_momentum, b.angular_momentum); + Same(a.momentum_rate, b.momentum_rate); + Same(a.angular_momentum_rate, b.angular_momentum_rate); + Same(a.momentum_rate_scale, b.momentum_rate_scale); + Same(a.angular_momentum_rate_scale, b.angular_momentum_rate_scale); + Same(v, saved_v); + Same(acceleration, saved_acceleration); + const auto failed_direct = kernel.Value().Evaluate(trial, fesa::Vector{23}, acceleration); + const auto failed_via = capability->Evaluate(trial, fesa::Vector{23}, acceleration); + ASSERT_FALSE(failed_direct.HasValue()); + ASSERT_FALSE(failed_via.HasValue()); + Same(failed_direct.GetStatus(), failed_via.GetStatus()); +} + +TEST(ElementCapabilities, ThermalPreservesStationLayoutIntervalsAndAllTraces) { + const std::array x{{{0., 0., 0.}, {1., 0., 0.}, + {1., 1., 0.}, {0., 1., 0.}}}; + const auto kernel = fesa::ThermalShell::Create( + {"thermal", 19, "0019"}, {7, 2, 9, 4}, x, 2., 3., 4., .5); + ASSERT_TRUE(kernel.HasValue()); + std::unique_ptr capability = + std::make_unique(kernel.Value()); + EXPECT_EQ(capability->DofLayout().source_id.source_label_text, "0019"); + EXPECT_EQ(capability->DofLayout().node_indices, kernel.Value().NodeIndices()); + for (std::size_t i = 0; i < 5; ++i) + EXPECT_EQ(static_cast(capability->DofLayout().components[i]), 11 + i); + EXPECT_EQ(&capability->Conductivity(), &kernel.Value().Conductivity()); + EXPECT_EQ(&capability->Capacity(), &kernel.Value().Capacity()); + EXPECT_EQ(&capability->ConductivityError(), &kernel.Value().ConductivityError()); + EXPECT_EQ(&capability->CapacityError(), &kernel.Value().CapacityError()); + std::array temperature{}; + for (std::size_t i = 0; i < 20; ++i) temperature[i] = static_cast(i * i); + const auto saved = temperature; + const auto direct = kernel.Value().Recover(temperature); + const auto via = capability->Recover(temperature); + ASSERT_TRUE(direct.HasValue()); + ASSERT_TRUE(via.HasValue()); + for (std::size_t i = 0; i < 24; ++i) + Same(direct.Value().global_flux[i], via.Value().global_flux[i]); + for (std::size_t p = 0; p < 4; ++p) + for (std::size_t i = 0; i < 3; ++i) + Same(direct.Value().frames[p][i], via.Value().frames[p][i]); + EXPECT_EQ(temperature, saved); + temperature[9] = std::numeric_limits::infinity(); + const auto failed_direct = kernel.Value().Recover(temperature); + const auto failed_via = capability->Recover(temperature); + ASSERT_FALSE(failed_direct.HasValue()); + ASSERT_FALSE(failed_via.HasValue()); + Same(failed_direct.GetStatus(), failed_via.GetStatus()); +} + +fesa::Result Solid() { + const fesa::Hex8Element::Coordinates x{{ + {-1., -1., -1.}, {1., -1., -1.}, {1., 1., -1.}, {-1., 1., -1.}, + {-1., -1., 1.}, {1., -1., 1.}, {1., 1., 1.}, {-1., 1., 1.}}}; + return fesa::Hex8Element::Create( + 3, {{"solid", 42, "0042"}, {7, 2, 9, 4, 8, 3, 10, 5}, + {fesa::DofComponent::kUx, fesa::DofComponent::kUy, + fesa::DofComponent::kUz}}, + x, fesa::LinearElasticMaterial{"elastic", 120., .25, {}}, 2.); +} +void Same(const fesa::Hex8Response& a, const fesa::Hex8Response& b) { + Same(a.internal_force, b.internal_force); + Same(a.material_tangent, b.material_tangent); + Same(a.geometric_tangent, b.geometric_tangent); + Same(a.tangent, b.tangent); + Same(a.strain_energy, b.strain_energy); + for (std::size_t i = 0; i < 8; ++i) { + const auto& x = a.points[i]; + const auto& y = b.points[i]; + EXPECT_EQ(x.element, y.element); + EXPECT_EQ(x.integration_point, y.integration_point); + Same(x.natural_coordinates, y.natural_coordinates); + Same(x.reference_coordinates, y.reference_coordinates); + Same(x.strain, y.strain); + Same(x.cauchy_stress, y.cauchy_stress); + } +} + +TEST(ElementCapabilities, SolidRetainsLinearNonlinearAndPairedResponses) { + auto made = Solid(); + ASSERT_TRUE(made.HasValue()); + const auto& kernel = made.Value(); + const fesa::Hex8TrialAdapter adapter{kernel}; + const fesa::SolidTrialElement& capability = adapter; + const fesa::Element& base = capability; + EXPECT_EQ(&base.DofLayout(), &kernel.DofLayout()); + fesa::Vector u{24}, low{24}; + for (std::size_t i = 0; i < 24; ++i) { + u[i] = .001 * static_cast(i + 1); + low[i] = 1e-20 * static_cast(i + 1); + } + const auto saved_u = u, saved_low = low; + const auto linear = kernel.EvaluateLinear(u); + const auto via_linear = capability.EvaluateLinear(u); + const auto nonlinear = kernel.EvaluateNonlinear(u); + const auto via_nonlinear = capability.EvaluateNonlinear(u); + ASSERT_TRUE(linear.HasValue()); + ASSERT_TRUE(via_linear.HasValue()); + ASSERT_TRUE(nonlinear.HasValue()); + ASSERT_TRUE(via_nonlinear.HasValue()); + Same(linear.Value(), via_linear.Value()); + Same(nonlinear.Value(), via_nonlinear.Value()); + for (const bool nonlinear_path : {false, true}) { + const auto direct = nonlinear_path ? kernel.EvaluateNonlinearHighLow(u, low) + : kernel.EvaluateLinearHighLow(u, low); + const auto via = nonlinear_path ? capability.EvaluateNonlinearHighLow(u, low) + : capability.EvaluateLinearHighLow(u, low); + ASSERT_TRUE(direct.HasValue()); + ASSERT_TRUE(via.HasValue()); + Same(direct.Value().high, via.Value().high); + Same(direct.Value().internal_force_low, via.Value().internal_force_low); + Same(direct.Value().tangent_low, via.Value().tangent_low); + Same(direct.Value().strain_energy_low, via.Value().strain_energy_low); + } + const auto k = kernel.ComputeStiffness(), via_k = base.ComputeStiffness(); + const auto m = kernel.ComputeMass(), via_m = capability.ComputeMass(); + ASSERT_TRUE(k.HasValue()); + ASSERT_TRUE(via_k.HasValue()); + ASSERT_TRUE(m.HasValue()); + ASSERT_TRUE(via_m.HasValue()); + Same(k.Value().values, via_k.Value().values); + Same(m.Value().values, via_m.Value().values); + Same(kernel.ReferenceWeights(), capability.ReferenceWeights()); + const auto recovered = base.Recover(u), direct_recovered = kernel.Recover(u); + ASSERT_TRUE(recovered.HasValue()); + ASSERT_TRUE(direct_recovered.HasValue()); + EXPECT_EQ(recovered.Value().source_id.source_label_text, "0042"); + const auto& rows = std::get(recovered.Value().payload); + const auto& direct_rows = std::get(direct_recovered.Value().payload); + ASSERT_EQ(rows.rows.size(), direct_rows.rows.size()); + for (std::size_t i = 0; i < rows.rows.size(); ++i) { + EXPECT_EQ(rows.rows[i].element, direct_rows.rows[i].element); + EXPECT_EQ(rows.rows[i].integration_point, direct_rows.rows[i].integration_point); + Same(rows.rows[i].cauchy_stress, direct_rows.rows[i].cauchy_stress); + Same(rows.rows[i].strain, direct_rows.rows[i].strain); + } + Same(u, saved_u); + Same(low, saved_low); + const auto failed = capability.EvaluateNonlinear(fesa::Vector{23}); + const auto direct_failed = kernel.EvaluateNonlinear(fesa::Vector{23}); + ASSERT_FALSE(failed.HasValue()); + ASSERT_FALSE(direct_failed.HasValue()); + Same(failed.GetStatus(), direct_failed.GetStatus()); + const auto paired_failed = capability.EvaluateNonlinearHighLow(u, fesa::Vector{23}); + const auto direct_paired_failed = kernel.EvaluateNonlinearHighLow(u, fesa::Vector{23}); + ASSERT_FALSE(paired_failed.HasValue()); + ASSERT_FALSE(direct_paired_failed.HasValue()); + Same(paired_failed.GetStatus(), direct_paired_failed.GetStatus()); + const auto again = capability.EvaluateNonlinear(u); + ASSERT_TRUE(again.HasValue()); + Same(again.Value(), nonlinear.Value()); +} + +TEST(ElementCapabilities, BoundViewsSurviveOwnerVectorMovesAndRejectOtherFamilies) { + static_assert(std::has_virtual_destructor_v); + static_assert(std::has_virtual_destructor_v); + static_assert(std::has_virtual_destructor_v); + static_assert(std::has_virtual_destructor_v); + static_assert(std::has_virtual_destructor_v); + auto made = Solid(); + ASSERT_TRUE(made.HasValue()); + std::vector> owners; + owners.push_back(std::make_unique(std::move(made.Value()))); + const auto* original = owners[0].get(); + const fesa::ElementFactory factory; + auto capability = factory.BindSolidTrial(*original); + ASSERT_NE(capability, nullptr); + EXPECT_EQ(factory.BindShellReference(*original), nullptr); + // Reallocation moves unique_ptr owners, never the kernels they own. + owners.reserve(32); + auto transferred = std::move(owners); + EXPECT_EQ(transferred[0].get(), original); + EXPECT_EQ(&capability->DofLayout(), &original->DofLayout()); + const auto evaluated = capability->EvaluateLinear(fesa::Vector{24}); + ASSERT_TRUE(evaluated.HasValue()); + capability.reset(); // Destruction through the role must not delete its kernel. + const auto recovered = original->Recover(fesa::Vector{24}); + ASSERT_TRUE(recovered.HasValue()); + ShellInput input; + auto shell = fesa::Mitc4Shell::Create( + input.Pointers(), {{{0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}}}, + input.section, input.material); + ASSERT_TRUE(shell.HasValue()); + EXPECT_EQ(factory.BindSolidTrial(shell.Value()), nullptr); + auto reference = factory.BindShellReference(shell.Value()); + ASSERT_NE(reference, nullptr); + EXPECT_EQ(&reference->DofLayout(), &shell.Value().DofLayout()); + const auto direct_k = shell.Value().Stiffness(); + const auto via_k = reference->Stiffness(); + ASSERT_TRUE(direct_k.HasValue()); + ASSERT_TRUE(via_k.HasValue()); + Same(direct_k.Value().stabilized_global24, via_k.Value().stabilized_global24); +} + +} // namespace diff --git a/tests/unit/elements/finite_rotation_test.cpp b/tests/unit/elements/finite_rotation_test.cpp index 913ec79..bbf8248 100644 --- a/tests/unit/elements/finite_rotation_test.cpp +++ b/tests/unit/elements/finite_rotation_test.cpp @@ -161,6 +161,28 @@ TEST(FiniteRotation, RejectsCoordinatesInsidePrincipalBranchMargin) { "invalid-accepted-state"); } +TEST(FiniteRotation, SpatialMomentSolvePreservesKnownLoadAndVirtualWork) { + const Vector3 moment{2.0, -3.0, 4.0}; + const Vector3 variation{0.3, -0.5, 0.7}; + const double pi = std::acos(-1.0); + for (const double angle : {0.0, 1e-9, 0.7, pi - 1e-6}) { + const double a = angle == 0.0 ? 1.0 : std::sin(angle) / angle; + const double b = angle < 1e-8 ? angle / 2.0 : + (1.0 - std::cos(angle)) / angle; + // Independent z-axis J^T*m; the axial component is unchanged. + const Vector3 generalized{a * moment.X() + b * moment.Y(), + -b * moment.X() + a * moment.Y(), moment.Z()}; + const auto solved = SpatialMomentSO3(Vector3{0, 0, angle}, generalized); + ASSERT_TRUE(solved.HasValue()); + ExpectVectorNear(solved.Value(), moment, 1e-11); + const Vector3 spatial_variation{a * variation.X() - b * variation.Y(), + b * variation.X() + a * variation.Y(), + variation.Z()}; + EXPECT_NEAR(generalized.Dot(variation), + solved.Value().Dot(spatial_variation), 1e-11); + } +} + TEST(FiniteRotation, JacobiansAreFiniteInverseTransposeAndDerivativeConsistent) { const double pi = std::acos(-1.0); @@ -172,18 +194,13 @@ TEST(FiniteRotation, for (const Vector3& phi : rotations) { const Matrix left_jacobian = LeftJacobianSO3(phi); - const Matrix inverse_transpose = InverseTransposeLeftJacobianSO3(phi); EXPECT_TRUE(IsFinite(left_jacobian)); - EXPECT_TRUE(IsFinite(inverse_transpose)); - const Matrix identity = - Transpose(left_jacobian).Multiply(inverse_transpose); - for (std::size_t row = 0U; row < kRotationDimension; ++row) { - for (std::size_t column = 0U; column < kRotationDimension; ++column) { - EXPECT_NEAR(identity(row, column), row == column ? 1.0 : 0.0, - 1.0e-11); - } - } + const Vector3 load{0.7, -0.2, 0.4}; + const auto spatial = SpatialMomentSO3(phi, load); + ASSERT_TRUE(spatial.HasValue()); + ExpectVectorNear(Multiply(Transpose(left_jacobian), spatial.Value()), + load, 1e-11); const Vector3 director = Multiply(ExpSkew(phi), Vector3{0.0, 0.0, 1.0}); const Vector3 positive = diff --git a/tests/unit/elements/hex8_element_test.cpp b/tests/unit/elements/hex8_element_test.cpp index 7c055af..2d00739 100644 --- a/tests/unit/elements/hex8_element_test.cpp +++ b/tests/unit/elements/hex8_element_test.cpp @@ -811,36 +811,6 @@ TEST(Hex8Element, FiniteInputsThatOverflowDerivedArithmeticFailClosed) { std::string::npos); } -TEST(Hex8Element, BackendOperandRejectionRetainsOriginalStiffnessDiagnostic) { - Coordinates coordinates{}; - for (std::size_t a = 0; a < 8; ++a) - for (std::size_t i = 0; i < 3; ++i) - coordinates[a][i] = 0.01 * kSigns[a][i]; - // Original native probe: finite moduli/volume, but mu*gradient overflows. - // The failed diagnostic candidate must not become a scalar success fallback. - const auto element = Make(coordinates, 1e308, -0.5); - ASSERT_FALSE(element.HasValue()); - EXPECT_EQ(element.GetStatus().Diagnostics()[0].code, "invalid-hex8"); - EXPECT_EQ(element.GetStatus().Diagnostics()[0].message, - "Hex8 reference stiffness is nonfinite."); -} - -TEST(Hex8Element, TensorFailureRetainsPriorityOverEnergyAndTangentOverflow) { - Coordinates coordinates{}; - for (std::size_t a = 0; a < 8; ++a) - for (std::size_t i = 0; i < 3; ++i) - coordinates[a][i] = 0.01 * kSigns[a][i]; - const auto element = Make(coordinates, 1e200, -0.5); - ASSERT_TRUE(element.HasValue()); - const auto displacement = Affine(coordinates, - Tensor{{{1e100, 0, 0}, {0, 1e100, 0}, {0, 0, 1e100}}}); - const auto response = element.Value().EvaluateNonlinear(displacement); - ASSERT_FALSE(response.HasValue()); - EXPECT_EQ(response.GetStatus().Diagnostics()[0].code, "invalid-hex8"); - EXPECT_EQ(response.GetStatus().Diagnostics()[0].message, - "Hex8 trial tensor is nonfinite."); -} - // Independent Gauss-Legendre abscissae from polynomial root iteration. This // test does not call the kernel's shapes, Jacobian, inverse or quadrature. std::vector> LegendreRule(unsigned count) { @@ -1211,3 +1181,17 @@ TEST(Hex8Element, MeanVolumeWarpedLowOnlyStateRetainsWeightedCubicEnergy) { EXPECT_NE(actual_cubic.high, 0); } } // namespace + +TEST(Hex8Element, RejectsNonfiniteCanonicalJacobianActionBeforeFiniteGradients) { + auto coordinates = Brick(); + for (std::size_t a = 0; a < 8U; ++a) { + coordinates[a][0] = kSigns[a][0] * std::ldexp(1.0, -1025); + coordinates[a][1] = static_cast(kSigns[a][1]); + coordinates[a][2] = static_cast(kSigns[a][2]); + } + const auto rejected = Make(coordinates); + ASSERT_FALSE(rejected.HasValue()); + ASSERT_EQ(rejected.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].message, + "Reference Jacobian inverse is nonfinite."); +} diff --git a/tests/unit/elements/hex8_high_low_test.cpp b/tests/unit/elements/hex8_high_low_test.cpp index d1df9e7..add510b 100644 --- a/tests/unit/elements/hex8_high_low_test.cpp +++ b/tests/unit/elements/hex8_high_low_test.cpp @@ -2,839 +2,16 @@ #include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include "fesa/materials/isotropic_linear_elastic_material.h" #include "../../../src/fesa/math/high_low_arithmetic.h" #include "../../../src/fesa/elements/hex8_geometry.h" -#include "../../../src/fesa/elements/hex8_pair_arithmetic.h" -#include "../../../src/fesa/elements/hex8_pair_simd.h" -#include "../math/high_low_arithmetic_original.h" namespace { using fesa::math_internal::HighLow; using fesa::math_internal::Subtract; -std::uint64_t PairBits(double value) { - std::uint64_t bits = 0; - std::memcpy(&bits, &value, sizeof(bits)); - return bits; -} - -void ExpectOriginalProduct(HighLow left, HighLow right) { - const auto actual = fesa::hex8_internal::MultiplyPair(left, right); - const auto expected = fesa::high_low_original::Multiply( - {left.high, left.low}, {right.high, right.low}); - EXPECT_EQ(PairBits(actual.high), PairBits(expected.high)); - EXPECT_EQ(PairBits(actual.low), PairBits(expected.low)); - const auto actual_sum = fesa::hex8_internal::AddPair(left, right); - const auto expected_sum = fesa::high_low_original::Add( - {left.high, left.low}, {right.high, right.low}); - EXPECT_EQ(PairBits(actual_sum.high), PairBits(expected_sum.high)); - EXPECT_EQ(PairBits(actual_sum.low), PairBits(expected_sum.low)); -} - -TEST(Hex8HighLow, ScalarCrossProductShortcutMatchesOriginalBits) { - ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported()); - const double minimum = std::numeric_limits::denorm_min(); - const double maximum = std::numeric_limits::max(); - const double infinity = std::numeric_limits::infinity(); - const double nan = std::numeric_limits::quiet_NaN(); - std::vector pairs{{}, {-0.0, 0.0}, {0.0, -0.0}, {-0.0, -0.0}, - {1, -0.0}, {-1, -0.0}, {1, 1}, {0, minimum}, {infinity, 0}, - {0, infinity}, {nan, 0}, {0, nan}, {maximum}, {-maximum}, - {minimum}, {-minimum}, {std::numeric_limits::min()}, - {0.5}, {std::nextafter(0.5, 1.0)}, - {1, std::ldexp(1.0, -53)}, - {std::nextafter(1.0, 2.0), -std::ldexp(1.0, -53)}}; - for (const auto left : pairs) - for (const auto right : pairs) ExpectOriginalProduct(left, right); - for (int exponent = -1074; exponent <= 1023; ++exponent) { - const double value = std::ldexp(1.0, exponent); - for (double sign : {-1.0, 1.0}) - for (const auto right : pairs) { - ExpectOriginalProduct(HighLow{sign * value}, right); - ExpectOriginalProduct(right, HighLow{sign * value}); - } - } - std::mt19937_64 random(0x48e81234U); - for (int sample = 0; sample < 12000; ++sample) { - double left = 0; - double right = 0; - const auto left_bits = random(); - const auto right_bits = random(); - std::memcpy(&left, &left_bits, sizeof(left)); - std::memcpy(&right, &right_bits, sizeof(right)); - const auto pair = fesa::math_internal::TwoSum(left, right); - ExpectOriginalProduct(pair, HighLow{}); - ExpectOriginalProduct(HighLow{}, pair); - ExpectOriginalProduct(pair, HighLow{right}); - ExpectOriginalProduct(HighLow{right}, pair); - ExpectOriginalProduct(pair, pair); - } -} - -fesa::hex8_internal::PacketPair RawPacketPair(HighLow value) { - return {value.high, value.low}; -} - -fesa::hex8_internal::PairPacket FrozenMaterialPacket( - const fesa::hex8_internal::MaterialPacketInput& input) { - using Original = fesa::high_low_original::HighLow; - const auto original = [](fesa::hex8_internal::PacketPair value) { - return Original{value.high, value.low}; - }; - fesa::hex8_internal::PairPacket result; - for (std::size_t lane = 0; lane < 4; ++lane) { - const auto first = fesa::high_low_original::Multiply( - original(input.deviatoric_left.values[lane]), - original(input.deviatoric_right.values[lane])); - const auto second = fesa::high_low_original::Multiply( - original(input.shear_left.values[lane]), original(input.shear_right.values[lane])); - const auto third = fesa::high_low_original::Multiply( - original(input.tensor_left.values[lane]), original(input.gradient_dot.values[lane])); - const auto value = fesa::high_low_original::Add( - fesa::high_low_original::Add(first, second), third); - const auto accumulated = fesa::high_low_original::Add( - original(input.accumulated.values[lane]), - fesa::high_low_original::Multiply(value, original(input.weight))); - result.values[lane] = {accumulated.high, accumulated.low}; - } - return result; -} - -bool ExpectFrozenMaterialPacket( - const fesa::hex8_internal::MaterialPacketInput& input) { - const auto expected = FrozenMaterialPacket(input); - fesa::hex8_internal::PairPacket scalar, selected; - fesa::hex8_internal::EvaluateMaterialPacketScalar(input, &scalar); - const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator(); - evaluator(input, &selected); - for (std::size_t lane = 0; lane < 4; ++lane) { - SCOPED_TRACE(lane); - EXPECT_EQ(PairBits(scalar.values[lane].high), PairBits(expected.values[lane].high)); - EXPECT_EQ(PairBits(scalar.values[lane].low), PairBits(expected.values[lane].low)); - EXPECT_EQ(PairBits(selected.values[lane].high), PairBits(expected.values[lane].high)); - EXPECT_EQ(PairBits(selected.values[lane].low), PairBits(expected.values[lane].low)); - } - return evaluator != fesa::hex8_internal::EvaluateMaterialPacketScalar; -} - -TEST(Hex8HighLow, FourColumnMaterialPacketMatchesFrozenScalarBits) { - ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported()); - using fesa::hex8_internal::MaterialPacketInput; - RecordProperty("private_packet_backend", - fesa::hex8_internal::SelectMaterialPacketEvaluator() == - fesa::hex8_internal::EvaluateMaterialPacketScalar ? "scalar" : "avx2"); - std::size_t packet_checks = 0, avx2_evaluator_checks = 0; - const double tiny = std::numeric_limits::denorm_min(); - const double maximum = std::numeric_limits::max(); - const std::vector boundary{{}, {-0.0, 0}, {0, -0.0}, - {1}, {-1}, {1, -0.0}, {1, std::ldexp(1.0, -54)}, - {-1, std::ldexp(1.0, -54)}, {tiny}, {-tiny}, - {std::numeric_limits::min()}, {maximum}, {-maximum}, - {1, tiny}, {1, -tiny}, {std::nextafter(1.0, 2.0)}, - {1, 1}, {0, tiny}, {std::numeric_limits::infinity(), 0}, - {std::numeric_limits::quiet_NaN(), 0}, - {std::numeric_limits::signaling_NaN(), 0}, - {0, std::numeric_limits::signaling_NaN()}}; - for (std::size_t first = 0; first < boundary.size(); ++first) - for (std::size_t second = 0; second < boundary.size(); ++second) { - MaterialPacketInput input{}; - input.weight = {0.125}; - for (std::size_t lane = 0; lane < 4; ++lane) { - input.deviatoric_left.values[lane] = RawPacketPair(boundary[first]); - input.deviatoric_right.values[lane] = RawPacketPair(boundary[second]); - input.shear_left.values[lane] = {2, std::ldexp(1.0, -54)}; - input.shear_right.values[lane] = {-0.5, std::ldexp(1.0, -56)}; - input.tensor_left.values[lane] = {0.75}; - input.gradient_dot.values[lane] = {-0.25}; - input.accumulated.values[lane] = lane == 0 ? fesa::hex8_internal::PacketPair{0, 0} - : fesa::hex8_internal::PacketPair{0.125, 0}; - } - avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U; - ++packet_checks; - } - for (int exponent = -1074; exponent <= 1023; ++exponent) { - MaterialPacketInput input{}; - input.weight = {1}; - for (std::size_t lane = 0; lane < 4; ++lane) { - input.deviatoric_left.values[lane] = { - std::ldexp(lane % 2U == 0 ? 1.0 : -1.0, exponent)}; - input.deviatoric_right.values[lane] = {lane < 2 ? 0.5 : 2.0}; - } - avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U; - ++packet_checks; - } - std::mt19937_64 random(0x4c0116U); - for (std::size_t sample = 0; sample < 2048; ++sample) { - MaterialPacketInput input{}; - const auto pair = [&]() { - const double high = std::ldexp( - 1.0 + static_cast(random() % 256U) / 256.0, - static_cast(random() % 201U) - 100); - const double signed_high = (random() & 1U) != 0 ? high : -high; - const double low = sample % 3U == 0 ? 0.0 : std::ldexp(high, -55); - return fesa::hex8_internal::PacketPair{ - signed_high, (random() & 1U) != 0 ? low : -low}; - }; - input.weight = sample % 2U == 0 - ? fesa::hex8_internal::PacketPair{0.125, 0} : pair(); - for (std::size_t lane = 0; lane < 4; ++lane) { - input.deviatoric_left.values[lane] = pair(); - input.deviatoric_right.values[lane] = pair(); - input.shear_left.values[lane] = pair(); - input.shear_right.values[lane] = pair(); - input.tensor_left.values[lane] = pair(); - input.gradient_dot.values[lane] = pair(); - input.accumulated.values[lane] = sample % 4U == 0 - ? fesa::hex8_internal::PacketPair{0, 0} : pair(); - } - avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U; - ++packet_checks; - } - RecordProperty("packet_oracle_checks", static_cast(packet_checks)); - RecordProperty("avx2_evaluator_packet_checks", - static_cast(avx2_evaluator_checks)); -} - -TEST(Hex8HighLow, PacketSpecialLaneDoesNotChangeOtherColumns) { - fesa::hex8_internal::MaterialPacketInput input{}; - input.weight = {0.125}; - for (std::size_t lane = 0; lane < 4; ++lane) { - input.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)}; - input.deviatoric_right.values[lane] = {2, std::ldexp(1.0, -55)}; - input.shear_left.values[lane] = {0.5}; - input.shear_right.values[lane] = {-0.25}; - input.tensor_left.values[lane] = {0.75}; - input.gradient_dot.values[lane] = {0.125}; - } - const auto reference = FrozenMaterialPacket(input); - const std::vector special{{}, {-0.0, 0}, {1}, - {std::numeric_limits::max()}, - {std::numeric_limits::denorm_min()}, {1, 1}, - {std::numeric_limits::quiet_NaN(), 0}}; - for (std::size_t lane = 0; lane < 4; ++lane) - for (const auto value : special) { - auto changed = input; - changed.deviatoric_left.values[lane] = RawPacketPair(value); - ExpectFrozenMaterialPacket(changed); - fesa::hex8_internal::PairPacket actual; - fesa::hex8_internal::SelectMaterialPacketEvaluator()(changed, &actual); - for (std::size_t other = 0; other < 4; ++other) - if (other != lane) { - EXPECT_EQ(PairBits(actual.values[other].high), PairBits(reference.values[other].high)); - EXPECT_EQ(PairBits(actual.values[other].low), PairBits(reference.values[other].low)); - } - } -} - -TEST(Hex8HighLow, UniformDyadicPacketsExerciseEachLowZeroShape) { - std::size_t checks = 0, avx2_checks = 0; - for (int shape = 0; shape < 4; ++shape) - for (int low_weight = 0; low_weight < 2; ++low_weight) - for (int zero_accumulator = 0; zero_accumulator < 2; ++zero_accumulator) { - fesa::hex8_internal::MaterialPacketInput input{}; - input.weight = {0.5, low_weight != 0 ? std::ldexp(1.0, -57) : 0.0}; - for (std::size_t lane = 0; lane < 4; ++lane) { - const bool left_low = (shape & 1) != 0; - const bool right_low = (shape & 2) != 0; - // Every lane has the same normalized nonzero dyadic inputs. Component - // LBE sums stay above -200 and all Add intermediates are bounded by 4. - // Thus each Multiply low-shape is uniform and its component products - // bypass zero/unsupported-tail fallback. No mixed-zero Add fallback - // is possible; the initial zero accumulator is uniform across lanes. - input.deviatoric_left.values[lane] = {1, left_low ? std::ldexp(1.0, -54) : 0}; - input.deviatoric_right.values[lane] = {2, right_low ? std::ldexp(1.0, -54) : 0}; - input.shear_left.values[lane] = {0.5, left_low ? std::ldexp(1.0, -56) : 0}; - input.shear_right.values[lane] = {0.25, right_low ? std::ldexp(1.0, -57) : 0}; - input.tensor_left.values[lane] = {0.75, left_low ? std::ldexp(1.0, -56) : 0}; - input.gradient_dot.values[lane] = {0.125, right_low ? std::ldexp(1.0, -58) : 0}; - input.accumulated.values[lane] = zero_accumulator != 0 - ? fesa::hex8_internal::PacketPair{0, 0} - : fesa::hex8_internal::PacketPair{0.0625, 0}; - } - avx2_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U; - ++checks; - } - RecordProperty("uniform_low_shape_checks", static_cast(checks)); - RecordProperty("avx2_uniform_low_shape_checks", static_cast(avx2_checks)); -} - -TEST(Hex8HighLow, OverflowLaneIsNeutralizedBeforeResidualFma) { - const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator(); - if (evaluator == fesa::hex8_internal::EvaluateMaterialPacketScalar) - GTEST_SKIP() << "AVX2 masked environment is not selected; routing unverified."; - std::fenv_t original; - ASSERT_EQ(std::fegetenv(&original), 0); - for (std::size_t bad = 0; bad < 4; ++bad) { - fesa::hex8_internal::MaterialPacketInput input{}; - input.weight = {0.5}; - for (std::size_t lane = 0; lane < 4; ++lane) { - input.deviatoric_left.values[lane] = { - lane == bad ? std::numeric_limits::max() : 1}; - input.deviatoric_right.values[lane] = {2}; - input.shear_left.values[lane] = {0.5}; - input.shear_right.values[lane] = {0.25}; - input.tensor_left.values[lane] = {0.75}; - input.gradient_dot.values[lane] = {0.125}; - input.accumulated.values[lane] = {0.0625}; - } - // All operands are finite/nonzero and max*2 has LBE sum 972, so Product - // reaches its vector multiply; only the post-product finite mask rejects it. - const auto expected = FrozenMaterialPacket(input); - EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0); - fesa::hex8_internal::PairPacket output; - evaluator(input, &output); - const bool extra_invalid = (std::fetestexcept(FE_INVALID) & FE_INVALID) != 0; - EXPECT_EQ(std::fesetenv(&original), 0); - EXPECT_FALSE(extra_invalid); - for (std::size_t lane = 0; lane < 4; ++lane) { - EXPECT_EQ(PairBits(output.values[lane].high), PairBits(expected.values[lane].high)); - EXPECT_EQ(PairBits(output.values[lane].low), PairBits(expected.values[lane].low)); - } - } - RecordProperty("avx2_product_overflow_positions", 4); -} - -#if defined(_M_X64) || defined(__SSE2__) -// Runs scalar and selected packet from identical FP state. The frozen original -// supplies raw-bit results; the unchanged baseline supplies FE/MXCSR status. -void ExpectPacketFlagsAndBits( - const fesa::hex8_internal::MaterialPacketInput& input, bool prior_status) { - std::fenv_t original; - ASSERT_EQ(std::fegetenv(&original), 0); - const auto original_csr = _mm_getcsr(); - const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator(); - struct Observation { - fesa::hex8_internal::PairPacket output; - int flags; - unsigned int status; - }; - const auto observe = [&](fesa::hex8_internal::MaterialPacketEvaluator backend) { - EXPECT_EQ(std::fesetenv(&original), 0); - _mm_setcsr(original_csr & ~0x3fU); - EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0); - if (prior_status) { - EXPECT_EQ(std::feraiseexcept(FE_INEXACT), 0); - _mm_setcsr(_mm_getcsr() | 0x02U); // Preserve prior denormal status too. - } - Observation value{}; - backend(input, &value.output); - value.flags = std::fetestexcept(FE_ALL_EXCEPT); - value.status = _mm_getcsr() & 0x3fU; - return value; - }; - const auto scalar = observe(fesa::hex8_internal::EvaluateMaterialPacketScalar); - const auto selected = observe(evaluator); - const auto expected = FrozenMaterialPacket(input); - // Restore both x87 and MXCSR before assertions, including NaN bit assertions. - EXPECT_EQ(std::fesetenv(&original), 0); - _mm_setcsr(original_csr); - EXPECT_EQ(selected.flags, scalar.flags); - EXPECT_EQ(selected.status, scalar.status); - for (std::size_t lane = 0; lane < 4; ++lane) { - SCOPED_TRACE(lane); - EXPECT_EQ(PairBits(selected.output.values[lane].high), - PairBits(expected.values[lane].high)); - EXPECT_EQ(PairBits(selected.output.values[lane].low), - PairBits(expected.values[lane].low)); - EXPECT_EQ(PairBits(scalar.output.values[lane].high), - PairBits(expected.values[lane].high)); - EXPECT_EQ(PairBits(scalar.output.values[lane].low), - PairBits(expected.values[lane].low)); - } -} - -fesa::hex8_internal::MaterialPacketInput DyadicPacketFixture() { - fesa::hex8_internal::MaterialPacketInput input{}; - input.weight = {0.5, 0}; - for (std::size_t lane = 0; lane < 4; ++lane) { - input.deviatoric_left.values[lane] = {1, 0}; - input.deviatoric_right.values[lane] = {2, 0}; - input.shear_left.values[lane] = {0.5, 0}; - input.shear_right.values[lane] = {0.25, 0}; - input.tensor_left.values[lane] = {0.75, 0}; - input.gradient_dot.values[lane] = {0.125, 0}; - } - return input; -} - -TEST(Hex8HighLow, PacketVectorGuardsPreserveScalarFlagsAndBits) { - if (fesa::hex8_internal::SelectMaterialPacketEvaluator() == - fesa::hex8_internal::EvaluateMaterialPacketScalar) - GTEST_SKIP() << "AVX2 masked environment is not selected."; - using fesa::hex8_internal::PacketPair; - const double maximum = std::numeric_limits::max(); - const double snan = std::numeric_limits::signaling_NaN(); - const double tiny = std::numeric_limits::denorm_min(); - const std::vector invalid_left{ - {-0.0, 0}, {0, -0.0}, {1, 1}, {snan, 0}, {0, snan}}; - const std::vector dangerous_right{ - {maximum, maximum}, {snan, 0}, {0, snan}}; - std::size_t checks = 0; - for (const auto left : invalid_left) - for (const auto right : dangerous_right) - for (std::size_t bad = 0; bad < 4; ++bad) { - auto input = DyadicPacketFixture(); - input.deviatoric_left.values[bad] = left; - input.deviatoric_right.values[bad] = right; - // Later lanes must still contribute the unchanged bridge's RN status. - const auto later = (bad + 1) % 4; - input.deviatoric_left.values[later] = {1, std::ldexp(1.0, -54)}; - input.deviatoric_right.values[later] = {tiny, 0}; - ExpectPacketFlagsAndBits(input, false); - ExpectPacketFlagsAndBits(input, true); - checks += 2; - } - for (std::size_t lane = 0; lane < 4; ++lane) { - auto mixed = DyadicPacketFixture(); - mixed.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)}; - mixed.deviatoric_left.values[(lane + 1) % 4] = {maximum, maximum}; - ExpectPacketFlagsAndBits(mixed, false); - ExpectPacketFlagsAndBits(mixed, true); - auto signed_low = DyadicPacketFixture(); - signed_low.deviatoric_left.values[lane].low = -0.0; - signed_low.accumulated.values[lane] = {-0.0, 0}; - ExpectPacketFlagsAndBits(signed_low, false); - checks += 3; - } - RecordProperty("avx2_guard_flags_and_bit_checks", static_cast(checks)); -} - -TEST(Hex8HighLow, PacketTailScreenRetainsExactBoundaryAndFallback) { - if (fesa::hex8_internal::SelectMaterialPacketEvaluator() == - fesa::hex8_internal::EvaluateMaterialPacketScalar) - GTEST_SKIP() << "AVX2 masked environment is not selected."; - const auto odd = [](int exponent) { - return std::nextafter(std::ldexp(1.0, exponent), - std::numeric_limits::infinity()); - }; - const double tiny = std::numeric_limits::denorm_min(); - const std::array, 8> operands{{ - {odd(-500), odd(-470)}, // screen true, exact LBE sum -1074 - {odd(-500), odd(-471)}, // screen false, exact rejection -1075 - {std::ldexp(1.0, -500), std::ldexp(1.0, -471)}, // false but supported - {tiny, odd(52)}, {tiny, odd(51)}, {3 * tiny, odd(52)}, - {std::numeric_limits::max(), 2}, {0, odd(52)}}}; - std::size_t checks = 0; - for (const auto& pair : operands) - for (std::size_t special = 0; special < 4; ++special) { - fesa::hex8_internal::MaterialPacketInput input{}; - input.weight = {1, 0}; - for (std::size_t lane = 0; lane < 4; ++lane) { - const double sign = lane % 2 == 0 ? 1.0 : -1.0; - input.deviatoric_left.values[lane] = - {sign * (lane == special ? pair[0] : odd(-500)), 0}; - input.deviatoric_right.values[lane] = - {lane == special ? pair[1] : odd(-470), 0}; - } - ExpectPacketFlagsAndBits(input, false); - ExpectPacketFlagsAndBits(input, true); - checks += 2; - } - RecordProperty("avx2_tail_screen_flags_and_bit_checks", static_cast(checks)); -} -#endif - -#if defined(_M_X64) || defined(__SSE2__) -fesa::hex8_internal::GeometryPacketOutput FrozenGeometryPacket( - const fesa::hex8_internal::GeometryPacketInput& input) { - const auto pair = [](fesa::hex8_internal::PacketPair value) { - return fesa::high_low_original::HighLow{value.high, value.low}; - }; - fesa::hex8_internal::GeometryPacketOutput output{}; - for (std::size_t lane = 0; lane < 4; ++lane) { - fesa::high_low_original::HighLow dot; - for (std::size_t i = 0; i < 3; ++i) - dot = fesa::high_low_original::Add(dot, - fesa::high_low_original::Multiply(pair(input.gradient_a[i]), - pair(input.gradient_b[i].values[lane]))); - fesa::high_low_original::HighLow geo; - for (std::size_t j = 0; j < 3; ++j) - for (std::size_t l = 0; l < 3; ++l) - geo = fesa::high_low_original::Add(geo, - fesa::high_low_original::Multiply(pair(input.gradient_stress[j][l]), - pair(input.gradient_b[l].values[lane]))); - const auto weighted = fesa::high_low_original::Multiply(geo, pair(input.weight)); - output.gradient_dot.values[lane] = {dot.high, dot.low}; - output.weighted_geometric.values[lane] = {weighted.high, weighted.low}; - } - return output; -} - -fesa::hex8_internal::GeometryPacketInput GeometryDyadicFixture() { - fesa::hex8_internal::GeometryPacketInput input{}; - input.weight = {0.125, 0}; - for (std::size_t j = 0; j < 3; ++j) { - input.gradient_a[j] = {static_cast(j + 1) * 0.25, 0}; - for (std::size_t l = 0; l < 3; ++l) { - // Deliberately asymmetric first-products and distinct b/component mapping. - input.gradient_stress[j][l] = - {static_cast(1 + 3 * j + l) * 0.125, 0}; - for (std::size_t lane = 0; lane < 4; ++lane) - input.gradient_b[l].values[lane] = - {static_cast(1 + 4 * l + lane) * 0.0625, 0}; - } - } - return input; -} - -// Original observation repeats geo*weight for three independent accumulators. -// Selected observation reuses the weighted pair but retains each checked Add. -void ExpectGeometryPacketBitsAndFlags( - const fesa::hex8_internal::GeometryPacketInput& input, bool prior_status) { - SCOPED_TRACE(prior_status); - std::fenv_t original; - ASSERT_EQ(std::fegetenv(&original), 0); - const auto original_csr = _mm_getcsr(); - struct Observation { - fesa::hex8_internal::GeometryPacketOutput output; - fesa::hex8_internal::PairPacket accumulators[3]; - int flags; - unsigned int status; - }; - const auto current_pair = [](fesa::hex8_internal::PacketPair value) { - return HighLow{value.high, value.low}; - }; - const auto observe = [&](bool original_order) { - EXPECT_EQ(std::fesetenv(&original), 0); - _mm_setcsr(original_csr & ~0x3fU); - EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0); - if (prior_status) { - EXPECT_EQ(std::feraiseexcept(FE_INEXACT), 0); - _mm_setcsr(_mm_getcsr() | 0x02U); - } - Observation value{}; - if (!original_order) - fesa::hex8_internal::EvaluateGeometryPacketAvx2(input, &value.output); - for (std::size_t lane = 0; lane < 4; ++lane) { - HighLow geo; - if (original_order) { - HighLow dot; - for (std::size_t i = 0; i < 3; ++i) - dot = fesa::hex8_internal::AddPair(dot, - fesa::hex8_internal::MultiplyPair(current_pair(input.gradient_a[i]), - current_pair(input.gradient_b[i].values[lane]))); - for (std::size_t j = 0; j < 3; ++j) - for (std::size_t l = 0; l < 3; ++l) - geo = fesa::hex8_internal::AddPair(geo, - fesa::hex8_internal::MultiplyPair( - current_pair(input.gradient_stress[j][l]), - current_pair(input.gradient_b[l].values[lane]))); - value.output.gradient_dot.values[lane] = {dot.high, dot.low}; - } - const HighLow accumulators[3]{{}, - {static_cast(lane + 1), std::ldexp(1.0, -54)}, {-1, -0.0}}; - for (std::size_t component = 0; component < 3; ++component) { - const auto weighted = original_order - ? fesa::hex8_internal::MultiplyPair(geo, current_pair(input.weight)) - : current_pair(value.output.weighted_geometric.values[lane]); - if (original_order) - value.output.weighted_geometric.values[lane] = - {weighted.high, weighted.low}; - const auto accumulated = fesa::hex8_internal::AddPair( - accumulators[component], weighted); - value.accumulators[component].values[lane] = - {accumulated.high, accumulated.low}; - } - } - value.flags = std::fetestexcept(FE_ALL_EXCEPT); - value.status = _mm_getcsr() & 0x3fU; - return value; - }; - const auto baseline = observe(true); - const auto selected = observe(false); - fesa::hex8_internal::GeometryPacketOutput scalar; - fesa::hex8_internal::EvaluateGeometryPacketScalar(input, &scalar); - const auto expected = FrozenGeometryPacket(input); - EXPECT_EQ(std::fesetenv(&original), 0); - _mm_setcsr(original_csr); - EXPECT_EQ(selected.flags, baseline.flags); - EXPECT_EQ(selected.status, baseline.status); - const auto same_bits = [](const fesa::hex8_internal::PairPacket& a, - const fesa::hex8_internal::PairPacket& b) { - for (std::size_t lane = 0; lane < 4; ++lane) { - EXPECT_EQ(PairBits(a.values[lane].high), PairBits(b.values[lane].high)); - EXPECT_EQ(PairBits(a.values[lane].low), PairBits(b.values[lane].low)); - } - }; - same_bits(selected.output.gradient_dot, expected.gradient_dot); - same_bits(selected.output.weighted_geometric, expected.weighted_geometric); - same_bits(scalar.gradient_dot, expected.gradient_dot); - same_bits(scalar.weighted_geometric, expected.weighted_geometric); - for (std::size_t component = 0; component < 3; ++component) - same_bits(selected.accumulators[component], baseline.accumulators[component]); -} - -TEST(Hex8HighLow, GeometryPacketPreservesAsymmetricLanesAndUniformShapes) { - if (fesa::hex8_internal::SelectMaterialPacketEvaluator() == - fesa::hex8_internal::EvaluateMaterialPacketScalar) - GTEST_SKIP() << "Geometry AVX2 masked environment is not selected."; - RecordProperty("private_geometry_packet_backend", "avx2"); - static_assert(std::is_trivial_v); - static_assert(std::is_trivial_v); - std::size_t checks = 0; - for (int shape = 0; shape < 4; ++shape) { - auto input = GeometryDyadicFixture(); - for (std::size_t j = 0; j < 3; ++j) { - if ((shape & 1) != 0) { - input.gradient_a[j].low = std::ldexp(input.gradient_a[j].high, -55); - for (std::size_t l = 0; l < 3; ++l) - input.gradient_stress[j][l].low = - std::ldexp(input.gradient_stress[j][l].high, -55); - } - if ((shape & 2) != 0) - for (std::size_t lane = 0; lane < 4; ++lane) - input.gradient_b[j].values[lane].low = - std::ldexp(input.gradient_b[j].values[lane].high, -55); - } - ExpectGeometryPacketBitsAndFlags(input, false); - ExpectGeometryPacketBitsAndFlags(input, true); - checks += 2; - } - RecordProperty("avx2_geometry_uniform_bit_flag_checks", static_cast(checks)); -} - -TEST(Hex8HighLow, GeometryPacketRetainsSpecialLanesAndExactTailBoundary) { - if (fesa::hex8_internal::SelectMaterialPacketEvaluator() == - fesa::hex8_internal::EvaluateMaterialPacketScalar) - GTEST_SKIP() << "Geometry AVX2 masked environment is not selected."; - using fesa::hex8_internal::PacketPair; - const double tiny = std::numeric_limits::denorm_min(); - const double maximum = std::numeric_limits::max(); - const std::vector special{{0, 0}, {-0.0, 0}, {1, -0.0}, {1, 1}, - {tiny, 0}, {maximum, maximum}, - {std::numeric_limits::signaling_NaN(), 0}, - {0, std::numeric_limits::signaling_NaN()}}; - std::size_t checks = 0; - for (const auto pair : special) - for (std::size_t lane = 0; lane < 4; ++lane) { - auto input = GeometryDyadicFixture(); - input.gradient_b[1].values[lane] = pair; - ExpectGeometryPacketBitsAndFlags(input, false); - ExpectGeometryPacketBitsAndFlags(input, true); - checks += 2; - } - const auto odd = [](int exponent) { - return std::nextafter(std::ldexp(1.0, exponent), - std::numeric_limits::infinity()); - }; - for (int exponent : {-470, -471}) - for (std::size_t lane = 0; lane < 4; ++lane) { - auto input = GeometryDyadicFixture(); - input.gradient_stress[0][0] = {odd(-500), 0}; - input.gradient_b[0].values[lane] = {odd(exponent), 0}; - ExpectGeometryPacketBitsAndFlags(input, false); - checks++; - } - RecordProperty("avx2_geometry_special_tail_checks", static_cast(checks)); -} - -TEST(Hex8HighLow, GeometryPacketPreservesOrderedOverflowAndPreparedInvalid) { - if (fesa::hex8_internal::SelectMaterialPacketEvaluator() == - fesa::hex8_internal::EvaluateMaterialPacketScalar) - GTEST_SKIP() << "Geometry AVX2 masked environment is not selected."; - const double maximum = std::numeric_limits::max(); - std::size_t checks = 0; - for (bool overflow_order : {false, true}) { - SCOPED_TRACE(overflow_order); - fesa::hex8_internal::GeometryPacketInput input{}; - input.weight = {0.5, 0}; - for (std::size_t l = 0; l < 3; ++l) - for (std::size_t lane = 0; lane < 4; ++lane) - input.gradient_b[l].values[lane] = {1, 0}; - input.gradient_stress[0][0] = {maximum, 0}; - input.gradient_stress[0][1] = {overflow_order ? maximum : -maximum, 0}; - input.gradient_stress[0][2] = {overflow_order ? -maximum : maximum, 0}; - ExpectGeometryPacketBitsAndFlags(input, false); - ExpectGeometryPacketBitsAndFlags(input, true); - checks += 2; - } - auto input = GeometryDyadicFixture(); - const auto invalid_first = fesa::high_low_original::Multiply( - {std::ldexp(1.0, -600), 0}, {std::ldexp(1.0, -500), 0}); - input.gradient_stress[0][0] = {invalid_first.high, invalid_first.low}; - for (std::size_t lane = 0; lane < 4; ++lane) - input.gradient_b[0].values[lane] = {std::ldexp(1.0, 600), 0}; - ExpectGeometryPacketBitsAndFlags(input, false); - ExpectGeometryPacketBitsAndFlags(input, true); - checks += 2; - RecordProperty("avx2_geometry_ordered_invalid_checks", static_cast(checks)); -} - -TEST(Hex8HighLow, GeometryPacketSumFallbackPreservesNearMaxAndSubnormalStatus) { - if (fesa::hex8_internal::SelectMaterialPacketEvaluator() == - fesa::hex8_internal::EvaluateMaterialPacketScalar) - GTEST_SKIP() << "Geometry AVX2 masked environment is not selected."; - const double maximum = std::numeric_limits::max(); - const double normal = std::ldexp(1.0, -1022); - const double tiny = std::ldexp(1.0, -1074); - const double quarter = std::ldexp(1.0, 969); - std::size_t checks = 0; - for (double tail : {0.0, normal, tiny}) - for (std::size_t bad = 0; bad < 4; ++bad) { - SCOPED_TRACE(tail); - SCOPED_TRACE(bad); - fesa::hex8_internal::GeometryPacketInput input{}; - input.weight = {0.5, 0}; - // Both pairs are normalized. Head sum is finite; middle contributes - // 2^970 and the joined sum overflows. Other lanes retain finite values. - input.gradient_stress[0][0] = {maximum, quarter}; - input.gradient_stress[0][1] = {quarter, tail}; - for (std::size_t lane = 0; lane < 4; ++lane) { - const double scale = lane == bad ? 1.0 : 0.5; - input.gradient_b[0].values[lane] = {scale, 0}; - input.gradient_b[1].values[lane] = {scale, 0}; - input.gradient_b[2].values[lane] = {1, 0}; - } - ExpectGeometryPacketBitsAndFlags(input, false); - ExpectGeometryPacketBitsAndFlags(input, true); - checks += 2; - } - fesa::hex8_internal::GeometryPacketInput finite{}; - finite.weight = {1, 0}; - finite.gradient_stress[0][0] = {normal, 0}; - finite.gradient_stress[0][1] = {normal + tiny, 0}; - for (std::size_t lane = 0; lane < 4; ++lane) { - finite.gradient_b[0].values[lane] = {1, 0}; - finite.gradient_b[1].values[lane] = {1, 0}; - } - // TwoSum(N,N+d) has finite normal inputs and a subnormal error d. - ExpectGeometryPacketBitsAndFlags(finite, false); - ExpectGeometryPacketBitsAndFlags(finite, true); - checks += 2; - // Mixed prefix: finite subnormal-error lanes run before another lane's head - // overflow. The bridge must preserve the whole packet's status union. - for (std::size_t bad = 0; bad < 4; ++bad) { - SCOPED_TRACE(bad); - fesa::hex8_internal::GeometryPacketInput mixed{}; - mixed.weight = {1, 0}; - mixed.gradient_stress[0][2] = {normal, 0}; - mixed.gradient_stress[1][2] = {normal + tiny, 0}; - mixed.gradient_stress[2][0] = {maximum, 0}; - mixed.gradient_stress[2][1] = {maximum, 0}; - for (std::size_t lane = 0; lane < 4; ++lane) { - mixed.gradient_b[0].values[lane] = {lane == bad ? 1.0 : 0.0, 0}; - mixed.gradient_b[1].values[lane] = {lane == bad ? 1.0 : 0.0, 0}; - mixed.gradient_b[2].values[lane] = {lane == bad ? 0.0 : 1.0, 0}; - } - ExpectGeometryPacketBitsAndFlags(mixed, false); - ExpectGeometryPacketBitsAndFlags(mixed, true); - checks += 2; - } - RecordProperty("avx2_geometry_sum_fallback_checks", static_cast(checks)); -} -#endif - -TEST(Hex8HighLow, PacketBackendSelectionRecoversAfterEnvironmentRestoration) { - const auto initial = fesa::hex8_internal::SelectMaterialPacketEvaluator(); - const int rounding = std::fegetround(); - ASSERT_EQ(std::fesetround(FE_UPWARD), 0); - EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(), - fesa::hex8_internal::EvaluateMaterialPacketScalar); - EXPECT_EQ(std::fesetround(rounding), 0); -#if defined(_M_X64) || defined(__SSE2__) - const auto control = _mm_getcsr(); - _mm_setcsr(control | 0x8040U); - EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(), - fesa::hex8_internal::EvaluateMaterialPacketScalar); - _mm_setcsr(control); - _mm_setcsr(control & ~0x80U); - const auto unmasked = fesa::hex8_internal::SelectMaterialPacketEvaluator(); - _mm_setcsr(control); - EXPECT_EQ(unmasked, fesa::hex8_internal::EvaluateMaterialPacketScalar); -#endif - EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(), initial); -} - -TEST(Hex8HighLow, PacketScalarBridgesPreserveOriginalRawBits) { - using fesa::hex8_internal::PacketPair; - using fesa::hex8_internal::PairPacket; - static_assert(std::is_trivial_v && - std::is_standard_layout_v); - static_assert(std::is_trivial_v && - std::is_standard_layout_v); - static_assert(std::is_trivial_v); - const double tiny = std::numeric_limits::denorm_min(); - const std::vector pairs{{0, 0}, {-0.0, 0}, {0, -0.0}, - {1, -0.0}, {1, std::ldexp(1.0, -54)}, {tiny, 0}, {1, tiny}, - {std::numeric_limits::max(), 0}, {1, 1}, - {std::numeric_limits::infinity(), 0}, - {std::numeric_limits::signaling_NaN(), 0}}; - for (const auto left : pairs) - for (const auto right : pairs) { - PairPacket a{{left, left, left, left}}; - PairPacket b{{right, right, right, right}}; - PairPacket result; - const fesa::high_low_original::HighLow x{left.high, left.low}; - const fesa::high_low_original::HighLow y{right.high, right.low}; - const auto expect = [&](fesa::high_low_original::HighLow expected) { - for (std::size_t lane = 0; lane < 4; ++lane) { - EXPECT_EQ(PairBits(result.values[lane].high), PairBits(expected.high)); - EXPECT_EQ(PairBits(result.values[lane].low), PairBits(expected.low)); - } - }; - fesa::hex8_internal::ScalarPacketAdd(a, b, false, &result); - expect(fesa::high_low_original::Add(x, y)); - fesa::hex8_internal::ScalarPacketAdd(a, b, true, &result); - expect(fesa::high_low_original::Add(x, y)); - fesa::hex8_internal::ScalarPacketMultiply(a, b, &result); - expect(fesa::high_low_original::Multiply(x, y)); - const double high_left[4]{left.high, left.high, left.high, left.high}; - const double high_right[4]{right.high, right.high, right.high, right.high}; - fesa::hex8_internal::ScalarPacketTwoProduct(high_left, high_right, &result); - expect(fesa::high_low_original::TwoProduct(left.high, right.high)); - } -} - -TEST(Hex8HighLow, PacketCapabilityRejectsMissingIntegerIsaAndOsState) { - constexpr std::uint32_t leaf1 = - (1U << 12) | (1U << 26) | (1U << 27) | (1U << 28); - constexpr std::uint32_t leaf7 = (1U << 3) | (1U << 5) | (1U << 8); - EXPECT_TRUE(fesa::hex8_internal::SupportsMaterialPacketInstructions( - leaf1, 6, leaf7)); - for (const auto missing : {1U << 3, 1U << 5, 1U << 8}) - EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions( - leaf1, 6, leaf7 & ~missing)); - for (const auto missing : {1U << 12, 1U << 26, 1U << 27, 1U << 28}) - EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions( - leaf1 & ~missing, 6, leaf7)); - for (std::uint64_t state : {0ULL, 2ULL, 4ULL}) - EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions( - leaf1, state, leaf7)); -} - -TEST(Hex8HighLow, PacketDoesNotClearPriorMaskedStickyFlags) { - std::fenv_t original; - ASSERT_EQ(std::fegetenv(&original), 0); - const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator(); - ASSERT_EQ(std::feraiseexcept(FE_INEXACT), 0); - fesa::hex8_internal::MaterialPacketInput input{}; - input.weight = {1}; - for (std::size_t lane = 0; lane < 4; ++lane) { - input.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)}; - input.deviatoric_right.values[lane] = {2, std::ldexp(1.0, -54)}; - } - fesa::hex8_internal::PairPacket output; - evaluator(input, &output); - const bool preserved = (std::fetestexcept(FE_INEXACT) & FE_INEXACT) != 0; - EXPECT_EQ(std::fesetenv(&original), 0); - EXPECT_TRUE(preserved); -} - fesa::Hex8Element UnitBrick(double offset = 0, double nu = 0) { fesa::ElementDofLayout layout; layout.node_indices = {0, 1, 2, 3, 4, 5, 6, 7}; @@ -850,116 +27,6 @@ fesa::Hex8Element UnitBrick(double offset = 0, double nu = 0) { return fesa::Hex8Element::Create(0, layout, coordinates, material, 2).Value(); } -void ExpectSamePairResponse(const fesa::Hex8HighLowResponse& actual, - const fesa::Hex8HighLowResponse& expected) { - EXPECT_EQ(PairBits(actual.high.strain_energy), - PairBits(expected.high.strain_energy)); - EXPECT_EQ(PairBits(actual.strain_energy_low), - PairBits(expected.strain_energy_low)); - ASSERT_EQ(actual.high.tangent.Rows(), 24U); - ASSERT_EQ(actual.tangent_low.Rows(), 24U); - for (std::size_t i = 0; i < 24; ++i) { - EXPECT_EQ(PairBits(actual.high.internal_force[i]), - PairBits(expected.high.internal_force[i])); - EXPECT_EQ(PairBits(actual.internal_force_low[i]), - PairBits(expected.internal_force_low[i])); - for (std::size_t j = 0; j < 24; ++j) { - EXPECT_EQ(PairBits(actual.high.tangent(i, j)), - PairBits(expected.high.tangent(i, j))); - EXPECT_EQ(PairBits(actual.tangent_low(i, j)), - PairBits(expected.tangent_low(i, j))); - } - } - for (std::size_t point = 0; point < 8; ++point) { - EXPECT_EQ(actual.high.points[point].integration_point, - expected.high.points[point].integration_point); - for (std::size_t i = 0; i < 6; ++i) { - EXPECT_EQ(PairBits(actual.high.points[point].cauchy_stress[i]), - PairBits(expected.high.points[point].cauchy_stress[i])); - EXPECT_EQ(PairBits(actual.high.points[point].strain[i]), - PairBits(expected.high.points[point].strain[i])); - } - } -} - -TEST(Hex8HighLow, ConcurrentFirstLinearTrialsAcrossCopiesAndMoveKeepBits) { - const auto element = UnitBrick(); - auto copied = element; - const auto moved = std::move(copied); - fesa::Vector high(24), low_a(24), low_b(24); - for (std::size_t i = 0; i < 24; ++i) high[i] = 1; - for (auto a : {1, 2, 5, 6}) { - low_a[3U * a] = std::ldexp(1.0, -60); - low_b[3U * a] = -std::ldexp(1.0, -59); - } - const auto oracle = UnitBrick(); - const auto expected_a = oracle.EvaluateLinearHighLow(high, low_a); - const auto expected_b = oracle.EvaluateLinearHighLow(high, low_b); - ASSERT_TRUE(expected_a.HasValue()); - ASSERT_TRUE(expected_b.HasValue()); - std::promise start; - const auto signal = start.get_future().share(); - std::atomic arrived{0U}; - std::array>, 4> calls; - for (std::size_t i = 0; i < calls.size(); ++i) - calls[i] = std::async(std::launch::async, [&, i] { - arrived.fetch_add(1U, std::memory_order_release); - signal.wait(); - return (i % 2U == 0U ? element : moved) - .EvaluateLinearHighLow(high, i < 2U ? low_a : low_b); - }); - while (arrived.load(std::memory_order_acquire) != calls.size()) - std::this_thread::yield(); - start.set_value(); - for (std::size_t i = 0; i < calls.size(); ++i) { - const auto response = calls[i].get(); - ASSERT_TRUE(response.HasValue()); - ExpectSamePairResponse(response.Value(), - i < 2U ? expected_a.Value() : expected_b.Value()); - } -} - -TEST(Hex8HighLow, FirstLinearPreparationRecoversAfterTrialAndEnvironmentFailure) { - ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported()); - const auto element = UnitBrick(); - fesa::Vector high(24), low(24); - EXPECT_FALSE(element.EvaluateLinearHighLow(fesa::Vector(23), low).HasValue()); - high[0] = std::numeric_limits::infinity(); - EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue()); - high[0] = 0; - struct RestoreEnvironment { - int rounding = std::fegetround(); -#if defined(_M_X64) || defined(__SSE2__) - unsigned csr = _mm_getcsr(); -#endif - ~RestoreEnvironment() { - std::fesetround(rounding); -#if defined(_M_X64) || defined(__SSE2__) - _mm_setcsr(csr); -#endif - } - } restore; - ASSERT_EQ(std::fesetround(FE_DOWNWARD), 0); - const auto bad_environment = element.EvaluateLinearHighLow(high, low); - ASSERT_FALSE(bad_environment.HasValue()); - EXPECT_EQ(bad_environment.GetStatus().Diagnostics()[0].message, - "Hex8 high+low requires nearest rounding and gradual underflow."); - ASSERT_EQ(std::fesetround(FE_TONEAREST), 0); -#if defined(_M_X64) || defined(__SSE2__) - _mm_setcsr(restore.csr | 0x8040U); - EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue()); - _mm_setcsr(restore.csr); -#endif - const auto recovered = element.EvaluateLinearHighLow(high, low); - ASSERT_TRUE(recovered.HasValue()); - high[3] = -2; - EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue()); - high[3] = 0; - const auto after_failure = element.EvaluateLinearHighLow(high, low); - ASSERT_TRUE(after_failure.HasValue()); - ExpectSamePairResponse(after_failure.Value(), recovered.Value()); -} - TEST(Hex8HighLow, LowOnlyAffineStrainSurvivesUniformHighTranslation) { auto brick = UnitBrick(); fesa::Vector high(24), low(24); diff --git a/tests/unit/elements/mitc4_shell_test.cpp b/tests/unit/elements/mitc4_shell_test.cpp index d90edc2..0cc2d9c 100644 --- a/tests/unit/elements/mitc4_shell_test.cpp +++ b/tests/unit/elements/mitc4_shell_test.cpp @@ -1173,3 +1173,16 @@ TEST(Mitc4ShellPhysicalRecovery, } EXPECT_NEAR(recovery.strain_energy, 72.16, 1.0e-12); } + +TEST(Mitc4ShellKinematics, RetainsReciprocalDeterminantOverflowGateAtTinyScale) { + const std::array nodes{ + Node(0, {0.0, 0.0, 0.0}), Node(1, {2e-104, 0.0, 0.0}), + Node(2, {2e-104, 2e-104, 0.0}), Node(3, {0.0, 2e-104, 0.0})}; + EXPECT_TRUE(fesa::Mitc4Shell::Create( + NodePointers(nodes), Directors(), Section(2e-99), Material()).HasValue()); + const auto rejected = fesa::Mitc4Shell::Create( + NodePointers(nodes), Directors(), Section(2e-102), Material()); + ASSERT_FALSE(rejected.HasValue()); + EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].code, + "invalid-shell-jacobian"); +} diff --git a/tests/unit/elements/nonlinear_mitc4_inertia_test.cpp b/tests/unit/elements/nonlinear_mitc4_inertia_test.cpp index 6257951..79f5761 100644 --- a/tests/unit/elements/nonlinear_mitc4_inertia_test.cpp +++ b/tests/unit/elements/nonlinear_mitc4_inertia_test.cpp @@ -494,25 +494,17 @@ TEST(DynamicRotation, InverseIdentityAndCurrentAnalyticNullAtFiniteRotation) { for (const V3 phi : {V3{0.5, 0.0, 0.0}, V3{0.2, -0.3, 0.1}}) { const auto rotation = fesa::ExpSkew(phi); const auto jacobian = fesa::LeftJacobianSO3(phi); - const auto inverse_transpose = fesa::InverseTransposeLeftJacobianSO3(phi); for (std::size_t column = 0; column < 3; ++column) { // R^T e_j is a unit initial director; its current director is e_j. const V3 initial{rotation(column, 0), rotation(column, 1), rotation(column, 2)}; const auto value = fesa::EvaluateDynamicRotation(phi, initial, {}, {}); ASSERT_TRUE(value.HasValue()); - const V3 inverse_column{inverse_transpose(column, 0), - inverse_transpose(column, 1), - inverse_transpose(column, 2)}; - EXPECT_LE( - (value.Value().drill_null - inverse_column / inverse_column.Norm()) - .Norm(), - 1.0e-12); - const V3 identity_column = - Multiply(jacobian, value.Value().drill_null) * inverse_column.Norm(); - for (std::size_t row = 0; row < 3; ++row) { - EXPECT_NEAR(identity_column[row], row == column ? 1.0 : 0.0, 1.0e-12); - } + const V3 mapped = Multiply(jacobian, value.Value().drill_null); + EXPECT_GT(mapped[column], 0.0); + EXPECT_NEAR(value.Value().drill_null.Norm(), 1.0, 1e-12); + for (std::size_t row = 0; row < 3; ++row) + if (row != column) EXPECT_NEAR(mapped[row], 0.0, 1e-12); } } for (const Fixture fixture : {Fixture{}, Warped()}) { diff --git a/tests/unit/fem/dof_manager_test.cpp b/tests/unit/fem/dof_manager_test.cpp index c93823c..0a95cce 100644 --- a/tests/unit/fem/dof_manager_test.cpp +++ b/tests/unit/fem/dof_manager_test.cpp @@ -14,6 +14,9 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/elements/element.h" + namespace fesa { class DofManagerTestAccess { diff --git a/tests/unit/io/hdf5/solid_hdf5_results_writer_test.cpp b/tests/unit/io/hdf5/solid_hdf5_results_writer_test.cpp index 1a3ea6b..92ce68a 100644 --- a/tests/unit/io/hdf5/solid_hdf5_results_writer_test.cpp +++ b/tests/unit/io/hdf5/solid_hdf5_results_writer_test.cpp @@ -26,6 +26,9 @@ #include "io/hdf5/hdf5_primitives.h" #include "io/hdf5/solid_hdf5_self_check.h" #include "solid_output_test_fixture.h" +#include "fesa/analysis/solid_hht_dynamic_procedure.h" +#include "fesa/analysis/solid_static_procedure.h" + namespace { using namespace fesa; using namespace fesa::hdf5_internal; diff --git a/tests/unit/math/dense_blas_internal_test.cpp b/tests/unit/math/dense_blas_internal_test.cpp index 7ddfb66..e08f0f4 100644 --- a/tests/unit/math/dense_blas_internal_test.cpp +++ b/tests/unit/math/dense_blas_internal_test.cpp @@ -1,9 +1,9 @@ #include "math/dense_blas_internal.h" +#include "math/jet_linear_solve.h" #include #include -#include #include #include #include @@ -11,129 +11,197 @@ 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); +// Forms RHS and residuals independently of the production dense adapter. +Matrix FactorRhs(const Matrix& a, const Matrix& x, const bool transpose, + const bool right) { + Matrix b{x.Rows(), x.Columns()}; + for (std::size_t i = 0; i < b.Rows(); ++i) + for (std::size_t j = 0; j < b.Columns(); ++j) + for (std::size_t k = 0; k < a.Rows(); ++k) + b(i, j) += right ? x(i, k) * (transpose ? a(j, k) : a(k, j)) + : (transpose ? a(k, i) : a(i, k)) * x(k, j); + return b; } -TEST(DenseBlasInternal, - OuterProductUsesRectangularStorageAndPreservesRejection) { - const std::array left{2.0, -3.0}; - const std::array right{4.0, -5.0, 0.25}; - std::array product{}; - ASSERT_TRUE(OuterProduct(left.data(), left.size(), right.data(), right.size(), - product.data())); - const std::array expected{8.0, -10.0, 0.5, -12.0, 15.0, -0.75}; - EXPECT_EQ(product, expected); - const auto accepted = product; - const std::array invalid{ - 1.0, std::numeric_limits::quiet_NaN()}; - EXPECT_FALSE(OuterProduct(invalid.data(), invalid.size(), right.data(), - right.size(), product.data())); - EXPECT_EQ(product, accepted); - EXPECT_TRUE(OuterProduct(nullptr, 0U, nullptr, 3U, nullptr)); - EXPECT_TRUE(OuterProduct(nullptr, 2U, nullptr, 0U, nullptr)); - EXPECT_THROW(OuterProduct(nullptr, 2U, right.data(), 3U, product.data()), - std::invalid_argument); - const auto too_large = - static_cast((std::numeric_limits::max)()) + 1U; - EXPECT_THROW(OuterProduct(nullptr, too_large, nullptr, 1U, nullptr), - std::length_error); - const auto too_many_elements = - std::numeric_limits::max() / sizeof(double) + 1U; - EXPECT_THROW(OuterProduct(nullptr, too_many_elements, nullptr, 1U, nullptr), - std::length_error); +void ExpectFactorSolution(const Matrix& a, const Matrix& x, const Matrix& b, + const Matrix& expected, const bool transpose, + const bool right) { + ASSERT_EQ(x.Rows(), expected.Rows()); + ASSERT_EQ(x.Columns(), expected.Columns()); + const auto residual = FactorRhs(a, x, transpose, right); + for (std::size_t i = 0; i < x.Rows(); ++i) + for (std::size_t j = 0; j < x.Columns(); ++j) { + EXPECT_DOUBLE_EQ(x(i, j), expected(i, j)); + EXPECT_DOUBLE_EQ(residual(i, j), b(i, j)); + } } -TEST(DenseBlasInternal, LocalDenseScopeRestoresTlsAfterProductsAndUnwind) { - 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 default_threads = mkl_get_max_threads(); - const std::array left{0.0, 2.0}; - const std::array right{3.0, 4.0}; - std::array product{}; - product.fill(std::numeric_limits::quiet_NaN()); - { - const ScopedLocalDenseThreads scope{}; - EXPECT_EQ(mkl_get_max_threads(), 1); - EXPECT_TRUE( - OuterProduct(left.data(), 2U, right.data(), 2U, product.data())); - EXPECT_DOUBLE_EQ(product[0U], 0.0); - EXPECT_DOUBLE_EQ(product[3U], 8.0); - } - EXPECT_EQ(mkl_get_max_threads(), default_threads); - EXPECT_EQ(mkl_set_num_threads_local(0), 0); - try { - const ScopedLocalDenseThreads scope{}; - OuterProduct(nullptr, 2U, right.data(), 2U, product.data()); - } catch (const std::invalid_argument&) { - } - EXPECT_EQ(mkl_set_num_threads_local(0), 0); - static_cast(mkl_set_num_threads_local(3)); - { - const ScopedLocalDenseThreads scope{}; - EXPECT_EQ(mkl_get_max_threads(), 3); - } - EXPECT_EQ(mkl_get_max_threads(), 3); +TEST(DenseFactor, LuSolvesRectangularRhsOnBothSidesAndTransposes) { + Matrix a{3U, 3U}; + const double values[]{0, 2, 1, 1, 1, 0, 2, 0, 1}; + for (std::size_t i = 0; i < 9U; ++i) a.Data()[i] = values[i]; + auto factor = DenseFactor::Lu(a); + ASSERT_TRUE(factor.HasValue()); + EXPECT_DOUBLE_EQ(factor.Value().Determinant(), -4.0); + for (const bool right : {false, true}) + for (const bool transpose : {false, true}) { + Matrix expected{right ? 2U : 3U, right ? 3U : 2U}; + for (std::size_t i = 0; i < 6U; ++i) + expected.Data()[i] = static_cast(i) - 2.0; + const auto b = FactorRhs(a, expected, transpose, right); + const auto solved = right ? factor.Value().SolveRight(b, transpose) + : factor.Value().Solve(b, transpose); + ASSERT_TRUE(solved.HasValue()); + ExpectFactorSolution(a, solved.Value(), b, expected, transpose, right); + } } -TEST(DenseBlasInternal, OuterProductLeavesFiniteInputOverflowForOwnerGate) { - const std::array left{std::numeric_limits::max()}; - const std::array right{2.0}; - std::array product{}; - EXPECT_TRUE(OuterProduct(left.data(), 1U, right.data(), 1U, product.data())); - EXPECT_TRUE(std::isinf(product[0U])); +TEST(DenseFactor, CholeskySolvesAndWhiteningRetainsScaleAndOrientation) { + Matrix a{2U, 2U}; + a(0, 0) = 4; + a(0, 1) = 2; + a(1, 0) = 2; + a(1, 1) = 5; + const auto factor = DenseFactor::Cholesky(a); + ASSERT_TRUE(factor.HasValue()); + EXPECT_DOUBLE_EQ(factor.Value().Determinant(), 16.0); + const auto lower = factor.Value().Lower(); + ASSERT_TRUE(lower.HasValue()); + EXPECT_DOUBLE_EQ(lower.Value()(0, 0), 2.0); + EXPECT_DOUBLE_EQ(lower.Value()(0, 1), 0.0); + EXPECT_DOUBLE_EQ(lower.Value()(1, 0), 1.0); + EXPECT_DOUBLE_EQ(lower.Value()(1, 1), 2.0); + for (const bool right : {false, true}) + for (const bool transpose : {false, true}) { + Matrix expected{right ? 3U : 2U, right ? 2U : 3U}; + for (std::size_t i = 0; i < 6U; ++i) + expected.Data()[i] = static_cast(i) - 2.0; + const auto b = FactorRhs(a, expected, transpose, right); + const auto solved = right ? factor.Value().SolveRight(b, transpose) + : factor.Value().Solve(b, transpose); + ASSERT_TRUE(solved.HasValue()); + ExpectFactorSolution(a, solved.Value(), b, expected, transpose, right); + const auto triangular_rhs = + FactorRhs(lower.Value(), expected, transpose, right); + const auto triangular = + right ? factor.Value().SolveLowerRight(triangular_rhs, transpose) + : factor.Value().SolveLower(triangular_rhs, transpose); + ASSERT_TRUE(triangular.HasValue()); + ExpectFactorSolution(lower.Value(), triangular.Value(), triangular_rhs, + expected, transpose, right); + } + // W = I * L^-T / sqrt(4); verify W^T (4*A) W = I without a procedure. + Matrix basis{3U, 2U}; + basis(0, 0) = 0.5; + basis(1, 1) = 0.5; + const auto w = factor.Value().SolveLowerRight(basis, true); + ASSERT_TRUE(w.HasValue()); + for (std::size_t i = 0; i < 2U; ++i) + for (std::size_t j = 0; j < 2U; ++j) { + double gram = 0; + for (std::size_t k = 0; k < 2U; ++k) + for (std::size_t l = 0; l < 2U; ++l) + gram += w.Value()(k, i) * (4 * a(k, l)) * w.Value()(l, j); + EXPECT_DOUBLE_EQ(gram, i == j ? 1.0 : 0.0); + } } -TEST(DenseBlasInternal, TransposeRightProductUsesInnerAndRectangularStrides) { - const std::array left{1, 2, 3, 4, 5, 6}; - const std::array right{7, 8, 9, 10, 11, 12, - 13, 14, 15, 16, 17, 18}; - std::array product; - ASSERT_TRUE(MultiplyTransposeRight(left.data(), 2U, right.data(), 4U, 3U, - product.data())); - const std::array expected{50, 68, 86, 104, 122, 167, 212, 257}; - EXPECT_EQ(product, expected); - EXPECT_TRUE( - MultiplyTransposeRight(nullptr, 2U, nullptr, 4U, 0U, product.data())); - for (const auto value : product) EXPECT_DOUBLE_EQ(value, 0.0); - auto nonfinite = right; - nonfinite[8U] = std::numeric_limits::infinity(); - product.fill(31.0); - EXPECT_FALSE(MultiplyTransposeRight(left.data(), 2U, nonfinite.data(), 4U, 3U, - product.data())); - for (const auto value : product) EXPECT_DOUBLE_EQ(value, 31.0); +TEST(DenseFactor, OwnsSnapshotAndFailedCandidatesPreserveReusableFactor) { + Matrix a{2U, 2U}; + a(0, 0) = 2; + a(1, 1) = 4; + auto factor = DenseFactor::Lu(a); + ASSERT_TRUE(factor.HasValue()); + a(0, 0) = 8; + const auto changed = DenseFactor::Lu(a); + ASSERT_TRUE(changed.HasValue()); + Matrix rhs{2U, 1U, 8}; + const auto old_solution = factor.Value().Solve(rhs); + ASSERT_TRUE(old_solution.HasValue()); + EXPECT_DOUBLE_EQ(old_solution.Value()(0, 0), 4); + EXPECT_DOUBLE_EQ(changed.Value().Solve(rhs).Value()(0, 0), 1); + EXPECT_FALSE(DenseFactor::Lu(Matrix{2U, 2U}).HasValue()); + EXPECT_FALSE(DenseFactor::Cholesky(Matrix{2U, 2U}).HasValue()); + a(0, 0) = -1; + EXPECT_FALSE(DenseFactor::Cholesky(a).HasValue()); + EXPECT_FALSE(DenseFactor::Lu(Matrix{2U, 3U}).HasValue()); + a(0, 0) = (std::numeric_limits::quiet_NaN)(); + EXPECT_FALSE(DenseFactor::Lu(a).HasValue()); + EXPECT_FALSE(DenseFactor::Cholesky(a).HasValue()); + EXPECT_FALSE(factor.Value().Solve(Matrix{3U, 1U}).HasValue()); + EXPECT_FALSE(factor.Value().SolveRight(Matrix{1U, 3U}).HasValue()); + EXPECT_FALSE(factor.Value().SolveLower(rhs).HasValue()); + rhs(0, 0) = (std::numeric_limits::infinity)(); + EXPECT_FALSE(factor.Value().Solve(rhs).HasValue()); + EXPECT_DOUBLE_EQ(old_solution.Value()(0, 0), 4); + rhs(0, 0) = 8; + EXPECT_DOUBLE_EQ(factor.Value().Solve(rhs).Value()(0, 0), 4); + Matrix tiny{1U, 1U, (std::numeric_limits::min)()}; + const auto tiny_factor = DenseFactor::Lu(tiny); + ASSERT_TRUE(tiny_factor.HasValue()); + EXPECT_FALSE(tiny_factor.Value() + .Solve(Matrix{1U, 1U, (std::numeric_limits::max)()}) + .HasValue()); + EXPECT_DOUBLE_EQ(tiny_factor.Value().Solve(tiny).Value()(0, 0), 1); +} + +TEST(DenseFactor, EmptyAndInfinityConditionBoundaryAreExplicit) { + for (const bool positive : {false, true}) { + const auto empty = positive ? DenseFactor::Cholesky(Matrix{0U, 0U}) + : DenseFactor::Lu(Matrix{0U, 0U}); + ASSERT_TRUE(empty.HasValue()); + const auto left = empty.Value().Solve(Matrix{0U, 3U}); + ASSERT_TRUE(left.HasValue()); + EXPECT_EQ(left.Value().Columns(), 3U); + const auto right = empty.Value().SolveRight(Matrix{4U, 0U}); + ASSERT_TRUE(right.HasValue()); + EXPECT_EQ(right.Value().Rows(), 4U); + EXPECT_FALSE(empty.Value().ReciprocalConditionInfinity().HasValue()); + for (const double diagonal : {0x1p-31, 0x1p-32, 0x1p-33}) { + Matrix a{2U, 2U}; + a(0, 0) = 1; + a(1, 1) = diagonal; + const auto factor = + positive ? DenseFactor::Cholesky(a) : DenseFactor::Lu(a); + ASSERT_TRUE(factor.HasValue()); + const auto rcond = factor.Value().ReciprocalConditionInfinity(); + ASSERT_TRUE(rcond.HasValue()); + EXPECT_DOUBLE_EQ(rcond.Value(), diagonal); + EXPECT_EQ(rcond.Value() >= 0x1p-32, diagonal >= 0x1p-32); + EXPECT_EQ(factor.Value().Solve(Matrix{2U, 0U}).Value().Columns(), 0U); + EXPECT_EQ(factor.Value().SolveRight(Matrix{0U, 2U}).Value().Rows(), 0U); + } + } +} + +TEST(DenseFactor, GeneralDimensionAndNonDiagonalConditionUseFullCoefficient) { + Matrix a{8U, 8U}; + a(0, 0) = 4; + for (std::size_t i = 1; i < 8U; ++i) { + a(i, i) = 5; + a(i, i - 1U) = 2; + a(i - 1U, i) = 2; + } + const auto factor = DenseFactor::Cholesky(a); + ASSERT_TRUE(factor.HasValue()); + Matrix expected{8U, 3U}; + for (std::size_t i = 0; i < 24U; ++i) + expected.Data()[i] = static_cast(i) - 8; + const auto rhs = FactorRhs(a, expected, false, false); + const auto solved = factor.Value().Solve(rhs); + ASSERT_TRUE(solved.HasValue()); + ExpectFactorSolution(a, solved.Value(), rhs, expected, false, false); + Matrix general{2U, 2U}; + general(0, 0) = 2; + general(0, 1) = 1; + general(1, 1) = 1; + const auto lu = DenseFactor::Lu(general); + ASSERT_TRUE(lu.HasValue()); + EXPECT_DOUBLE_EQ(lu.Value().ReciprocalConditionInfinity().Value(), 1.0 / 3.0); + EXPECT_FALSE(lu.Value().Lower().HasValue()); + Matrix overflow{2U, 2U, (std::numeric_limits::max)()}; + overflow(1, 0) = -overflow(1, 0); + EXPECT_FALSE(DenseFactor::Lu(overflow).HasValue()); } TEST(DenseBlasInternal, CDup003ConvertsZeroNormalAndOverflowLengths) { @@ -162,138 +230,199 @@ TEST(DenseBlasInternal, CDup003CopiesZeroAndNonzeroContiguousValues) { EXPECT_EQ(destination, source); } -TEST(DenseBlasInternal, TransposeProductChecksShapeAndKnownRectangularAction) { + +TEST(DenseBlasInternal, TransposeLeftMatchesIndependentRectangularProduct) { Matrix left{3U, 2U}; - Matrix right{3U, 1U}; + Matrix right{3U, 4U}; for (std::size_t row = 0; row < 3U; ++row) { - left(row, 0U) = static_cast(row + 1U); - left(row, 1U) = 2.0; - right(row, 0U) = static_cast(row); + for (std::size_t column = 0; column < 2U; ++column) + left(row, column) = static_cast(1U + row * 2U + column); + for (std::size_t column = 0; column < 4U; ++column) + right(row, column) = static_cast(row + column) - 2.0; } - const auto product = MultiplyTransposeLeft(left, right); + const Matrix product = MultiplyTransposeLeft(left, right); ASSERT_EQ(product.Rows(), 2U); - ASSERT_EQ(product.Columns(), 1U); - EXPECT_DOUBLE_EQ(product(0U, 0U), 8.0); - EXPECT_DOUBLE_EQ(product(1U, 0U), 6.0); - EXPECT_THROW(static_cast(MultiplyTransposeLeft(left, Matrix{2U, 1U})), + ASSERT_EQ(product.Columns(), 4U); + for (std::size_t row = 0; row < 2U; ++row) + for (std::size_t column = 0; column < 4U; ++column) { + double expected = 0.0; + for (std::size_t inner = 0; inner < 3U; ++inner) + expected += left(inner, row) * right(inner, column); + EXPECT_DOUBLE_EQ(product(row, column), expected); + } + const auto gram = MultiplyTransposeLeft(left, left); + EXPECT_DOUBLE_EQ(gram(0U, 0U), 35.0); + EXPECT_DOUBLE_EQ(gram(0U, 1U), 44.0); + EXPECT_DOUBLE_EQ(gram(1U, 1U), 56.0); +} + +TEST(DenseBlasInternal, TransposeLeftRetainsEmptyAndFailureContracts) { + const auto zeros = MultiplyTransposeLeft(Matrix{0U, 2U}, Matrix{0U, 3U}); + ASSERT_EQ(zeros.Rows(), 2U); + ASSERT_EQ(zeros.Columns(), 3U); + EXPECT_DOUBLE_EQ(zeros(1U, 2U), 0.0); + const auto empty = MultiplyTransposeLeft( + Matrix{(std::numeric_limits::max)(), 0U}, + Matrix{(std::numeric_limits::max)(), 0U}); + EXPECT_EQ(empty.Rows(), 0U); + EXPECT_EQ(empty.Columns(), 0U); + EXPECT_THROW(static_cast(MultiplyTransposeLeft(Matrix{1U, 2U}, + Matrix{2U, 1U})), std::invalid_argument); - const auto empty = MultiplyTransposeLeft(Matrix{0U, 2U}, Matrix{0U, 3U}); - EXPECT_EQ(empty.Rows(), 2U); - EXPECT_EQ(empty.Columns(), 3U); - EXPECT_DOUBLE_EQ(empty(1U, 2U), 0.0); - left(0U, 0U) = std::numeric_limits::quiet_NaN(); - EXPECT_THROW(static_cast(MultiplyTransposeLeft(left, right)), - std::invalid_argument); - left(0U, 0U) = 1.0; - right(0U, 0U) = std::numeric_limits::infinity(); - EXPECT_THROW(static_cast(MultiplyTransposeLeft(left, right)), + EXPECT_THROW(static_cast(MultiplyTransposeLeft( + Matrix{1U, 0U}, Matrix{1U, 1U, + (std::numeric_limits::quiet_NaN)()})), std::invalid_argument); } -TEST(DenseBlasInternal, DiagonalProductKeepsEntrywiseCancellationAndChecks) { - Vector diagonal{3U}; - diagonal[0U] = 4; - diagonal[1U] = 2; - diagonal[2U] = -1; - Vector input{3U}; - const double large = std::ldexp(1.0, 56); - input[0U] = 1; - input[1U] = -large; - input[2U] = 2 * large; - const auto product = MultiplyDiagonal(diagonal, input); - EXPECT_DOUBLE_EQ(product[0U] + (product[1U] - product[2U]), 4); - EXPECT_EQ(MultiplyDiagonal(Vector{0U}, Vector{0U}).Size(), 0U); - EXPECT_THROW(static_cast(MultiplyDiagonal(diagonal, Vector{2U})), - std::invalid_argument); - input[0U] = std::numeric_limits::infinity(); - EXPECT_THROW(static_cast(MultiplyDiagonal(diagonal, input)), - std::invalid_argument); - input[0U] = 1; - diagonal[0U] = std::numeric_limits::quiet_NaN(); - EXPECT_THROW(static_cast(MultiplyDiagonal(diagonal, input)), - std::invalid_argument); +TEST(DenseBlasInternal, RawProductsPreserveDestinationOnNonfiniteInput) { + const double left[]{1.0, 2.0, -3.0, 4.0}; + const double right[]{5.0, -6.0, 7.0, 8.0, -9.0, 10.0}; + std::array destination{}; + ASSERT_TRUE(MultiplyTransposeRight(left, 2U, right, 3U, 2U, + destination.data())); + EXPECT_EQ(destination, (std::array{-7.0, 23.0, 11.0, + -39.0, 11.0, 67.0})); + const double invalid[]{(std::numeric_limits::infinity)(), 1.0}; + destination.fill(42.0); + EXPECT_FALSE(MultiplyTransposeRight(invalid, 1U, right, 3U, 2U, + destination.data())); + EXPECT_FALSE(OuterProduct(left, 2U, invalid, 2U, destination.data())); + for (const double value : destination) EXPECT_DOUBLE_EQ(value, 42.0); + ASSERT_TRUE(OuterProduct(left, 2U, right, 3U, destination.data())); + EXPECT_EQ(destination, (std::array{5.0, -6.0, 7.0, + 10.0, -12.0, 14.0})); } -TEST(DenseBlasInternal, CholeskyAndInverseMatchKnownFactorsAndKeepFailedOutput) { - Matrix matrix{2U, 2U}; - matrix(0U, 0U) = 4; - matrix(0U, 1U) = 2; - matrix(1U, 0U) = 2; - matrix(1U, 1U) = 5; - Matrix lower{0U, 0U}, inverse{0U, 0U}; - ASSERT_TRUE(CholeskyLowerAndInverse(matrix, lower, inverse)); - EXPECT_DOUBLE_EQ(lower(0U, 0U), 2); - EXPECT_DOUBLE_EQ(lower(1U, 0U), 1); - EXPECT_DOUBLE_EQ(lower(1U, 1U), 2); - EXPECT_DOUBLE_EQ(lower(0U, 1U), 0); - EXPECT_DOUBLE_EQ(inverse(0U, 0U), .5); - EXPECT_DOUBLE_EQ(inverse(1U, 0U), -.25); - EXPECT_DOUBLE_EQ(inverse(1U, 1U), .5); - matrix(1U, 1U) = 0; - EXPECT_FALSE(CholeskyLowerAndInverse(matrix, lower, inverse)); - EXPECT_DOUBLE_EQ(lower(1U, 1U), 2); - EXPECT_DOUBLE_EQ(inverse(1U, 1U), .5); - EXPECT_THROW(static_cast(CholeskyLowerAndInverse( - Matrix{2U, 1U}, lower, inverse)), std::invalid_argument); - matrix(0U, 0U) = std::numeric_limits::quiet_NaN(); - EXPECT_THROW(static_cast(CholeskyLowerAndInverse(matrix, lower, inverse)), +TEST(DenseBlasInternal, RawProductZeroDimensionsAndOverflowAreExplicit) { + EXPECT_TRUE(OuterProduct(nullptr, 0U, nullptr, 3U, nullptr)); + EXPECT_TRUE(MultiplyTransposeRight(nullptr, 2U, nullptr, 0U, 3U, nullptr)); + std::array zeros{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + ASSERT_TRUE(MultiplyTransposeRight(nullptr, 2U, nullptr, 3U, 0U, + zeros.data())); + for (const double value : zeros) EXPECT_DOUBLE_EQ(value, 0.0); + EXPECT_THROW(MultiplyTransposeRight(nullptr, 1U, nullptr, 1U, 1U, nullptr), std::invalid_argument); + const auto overflow = + static_cast((std::numeric_limits::max)()) + 1U; + EXPECT_THROW(MultiplyTransposeRight(nullptr, overflow, nullptr, 0U, 0U, + nullptr), std::length_error); + double unchanged = 42.0; + EXPECT_THROW(CopyValues(&unchanged, overflow, &unchanged), std::length_error); + EXPECT_DOUBLE_EQ(unchanged, 42.0); } -TEST(DenseBlasInternal, - CholeskyFailureReasonPreservesCandidatesAndResetsOnReuse) { - Matrix matrix{2U, 2U}; - matrix(0U, 0U) = 4.0; - matrix(1U, 1U) = 9.0; - Matrix lower{0U, 0U}; - Matrix inverse{0U, 0U}; - auto reason = CholeskyFailure::kInverse; - ASSERT_TRUE(CholeskyLowerAndInverse(matrix, lower, inverse, &reason)); - EXPECT_EQ(reason, CholeskyFailure::kNone); - matrix(1U, 1U) = -1.0; - EXPECT_FALSE(CholeskyLowerAndInverse(matrix, lower, inverse, &reason)); - EXPECT_EQ(reason, CholeskyFailure::kPivot); - EXPECT_DOUBLE_EQ(lower(1U, 1U), 3.0); - EXPECT_DOUBLE_EQ(inverse(1U, 1U), 1.0 / 3.0); - matrix(1U, 1U) = 9.0; - EXPECT_TRUE(CholeskyLowerAndInverse(matrix, lower, inverse, &reason)); - EXPECT_EQ(reason, CholeskyFailure::kNone); - reason = CholeskyFailure::kInverse; - EXPECT_THROW(CholeskyLowerAndInverse(Matrix{2U, 1U}, lower, inverse, &reason), - std::invalid_argument); - EXPECT_EQ(reason, CholeskyFailure::kNone); -} - -TEST(DenseBlasInternal, SingleEquationScopeRestoresTlsAndRespectsOverrides) { - 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 default_threads = mkl_get_max_threads(); - { - const ScopedSingleEquationThreads scope{1U}; - EXPECT_EQ(mkl_get_max_threads(), 1); - } - EXPECT_EQ(mkl_get_max_threads(), default_threads); - EXPECT_EQ(mkl_set_num_threads_local(0), 0); - { - const ScopedSingleEquationThreads scope{2U}; - EXPECT_EQ(mkl_get_max_threads(), default_threads); - } - try { - const ScopedSingleEquationThreads scope{1U}; - throw std::runtime_error{"scope failure"}; - } catch (const std::runtime_error&) { - } - EXPECT_EQ(mkl_set_num_threads_local(0), 0); - static_cast(mkl_set_num_threads_local(3)); - { - const ScopedSingleEquationThreads scope{1U}; - EXPECT_EQ(mkl_get_max_threads(), 3); - } - EXPECT_EQ(mkl_get_max_threads(), 3); +TEST(DenseBlasInternal, ContiguousPrimitivesCheckEmptyAndOverflowBeforeUse) { + EXPECT_DOUBLE_EQ(DotValues(nullptr, 0U, nullptr), 0.0); + EXPECT_DOUBLE_EQ(NormValues(nullptr, 0U), 0.0); + EXPECT_NO_THROW(ScaleValues(2.0, nullptr, 0U)); + EXPECT_NO_THROW(AxpyValues(2.0, nullptr, 0U, nullptr)); + const auto overflow = + static_cast((std::numeric_limits::max)()) + 1U; + double unchanged = 42.0; + EXPECT_THROW(DotValues(nullptr, overflow, nullptr), std::length_error); + EXPECT_THROW(NormValues(nullptr, overflow), std::length_error); + EXPECT_THROW(ScaleValues(2.0, &unchanged, overflow), std::length_error); + EXPECT_THROW(AxpyValues(2.0, nullptr, overflow, &unchanged), std::length_error); + EXPECT_DOUBLE_EQ(unchanged, 42.0); } } // namespace } // namespace fesa::dense_blas_internal + +namespace fesa::math_internal { +namespace { +// Taylor coefficients of t^i s^j through total degree three. This independent +// polynomial oracle deliberately uses coefficients, not derivative channels. +struct Polynomial { + std::array, 4> c{}; +}; +Polynomial operator-(Polynomial a, const Polynomial& b) { + for (std::size_t i = 0; i <= 3; ++i) + for (std::size_t j = 0; i + j <= 3; ++j) a.c[i][j] -= b.c[i][j]; + return a; +} +Polynomial operator*(const Polynomial& a, const Polynomial& b) { + Polynomial result; + for (std::size_t i = 0; i <= 3; ++i) + for (std::size_t j = 0; i + j <= 3; ++j) + for (std::size_t k = 0; k <= i; ++k) + for (std::size_t l = 0; l <= j; ++l) + result.c[i][j] += a.c[k][l] * b.c[i-k][j-l]; + return result; +} +Polynomial PolynomialConstant(double value) { + Polynomial p; + p.c[0][0] = value; + return p; +} +double PolynomialRead(const Polynomial& p, std::size_t degree, + std::size_t channel) { + return p.c[channel][degree-channel]; +} +void PolynomialWrite(Polynomial& p, std::size_t degree, std::size_t channel, + double value) { + p.c[channel][degree-channel] = value; +} +TEST(JetLinearSolve, RecoversMixedCubicCoefficientsWithOnePrimalFactor) { + std::array, 3> a{}; + const double values[]{0, 2, 1, 1, 1, 0, 2, 0, 1}; + for (std::size_t i = 0; i < 3; ++i) + for (std::size_t j = 0; j < 3; ++j) { + a[i][j] = PolynomialConstant(values[3*i+j]); + for (std::size_t degree = 1; degree <= 3; ++degree) + for (std::size_t c = 0; c <= degree; ++c) + a[i][j].c[c][degree-c] = (static_cast(i+2*j+c)-2) / 16; + } + std::array expected{}, rhs{}; + for (std::size_t i = 0; i < 3; ++i) + for (std::size_t degree = 0; degree <= 3; ++degree) + for (std::size_t c = 0; c <= degree; ++c) + expected[i].c[c][degree-c] = (static_cast(i+degree+2*c)-3) / 8; + // Form the full product independently using exact dyadic long-double sums. + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t i = 0; i <= 3; ++i) + for (std::size_t j = 0; i+j <= 3; ++j) { + long double sum = 0; + for (std::size_t column = 0; column < 3; ++column) + for (std::size_t k = 0; k <= i; ++k) + for (std::size_t l = 0; l <= j; ++l) + sum += static_cast(a[row][column].c[k][l]) * + expected[column].c[i-k][j-l]; + rhs[row].c[i][j] = static_cast(sum); + } + Matrix primal{3, 3}; + for (std::size_t i = 0; i < 3; ++i) + for (std::size_t j = 0; j < 3; ++j) primal(i,j) = a[i][j].c[0][0]; + const auto factor = dense_blas_internal::DenseFactor::Lu(primal); + ASSERT_TRUE(factor.HasValue()); + std::array solved{}; + ASSERT_TRUE(SolveJetAction(a, rhs, factor.Value(), {1,2,3,4}, + PolynomialConstant, PolynomialRead, + PolynomialWrite, solved)); + for (std::size_t row = 0; row < 3; ++row) + for (std::size_t i = 0; i <= 3; ++i) + for (std::size_t j = 0; i+j <= 3; ++j) + EXPECT_NEAR(solved[row].c[i][j], expected[row].c[i][j], 2e-14); +} +TEST(JetLinearSolve, FailedChannelSolvePreservesCandidateAndInputs) { + std::array, 3> a{}; + Matrix primal{3, 3}; + for (std::size_t i = 0; i < 3; ++i) { + primal(i,i) = 1; + a[i][i] = PolynomialConstant(1); + } + const auto factor = dense_blas_internal::DenseFactor::Lu(primal); + ASSERT_TRUE(factor.HasValue()); + std::array rhs{}, solved{}; + rhs[1].c[1][0] = std::numeric_limits::infinity(); + solved[0].c[0][0] = 17; + EXPECT_FALSE(SolveJetAction(a, rhs, factor.Value(), {1,2,3,4}, + PolynomialConstant, PolynomialRead, + PolynomialWrite, solved)); + EXPECT_EQ(solved[0].c[0][0], 17); + EXPECT_EQ(a[1][1].c[0][0], 1); + EXPECT_EQ(rhs[1].c[1][0], std::numeric_limits::infinity()); +} +} // namespace +} // namespace fesa::math_internal diff --git a/tests/unit/math/high_low_arithmetic_test.cpp b/tests/unit/math/high_low_arithmetic_test.cpp index fc41fad..2727e24 100644 --- a/tests/unit/math/high_low_arithmetic_test.cpp +++ b/tests/unit/math/high_low_arithmetic_test.cpp @@ -1,15 +1,10 @@ #include "../../../src/fesa/math/high_low_arithmetic.h" +#include "elements/hex8_pair_simd.h" #include #include -#include -#include #include -#include -#include - -#include "high_low_arithmetic_original.h" namespace { using namespace fesa::math_internal; @@ -131,109 +126,81 @@ TEST(HighLowArithmetic, RejectsChangedRoundingAndFlushToZeroEnvironment) { EXPECT_FALSE(ArithmeticEnvironmentSupported()); #endif } +} // namespace -std::uint64_t Bits(const double value) { - std::uint64_t result = 0; - std::memcpy(&result, &value, sizeof(result)); - return result; -} - -double FromBits(const std::uint64_t bits) { - double result = 0; - std::memcpy(&result, &bits, sizeof(result)); - return result; -} - -TEST(HighLowArithmetic, FiniteClassificationMatchesBinary64Boundaries) { - const auto check = [](const double value) { - const bool expected = std::isfinite(value); - EXPECT_EQ(IsFinite(HighLow{value, 0.0}), expected); - EXPECT_EQ(IsFinite(HighLow{1.0, value}), expected); - }; - for (std::uint64_t exponent = 0; exponent < 2048U; ++exponent) { - for (const auto fraction : {0ULL, 1ULL, 1ULL << 51U, - (1ULL << 52U) - 1ULL}) { - for (const auto sign : {0ULL, 1ULL << 63U}) - check(FromBits(sign | (exponent << 52U) | fraction)); - } - } - std::mt19937_64 random{0x5800f17U}; - for (int sample = 0; sample < 10000; ++sample) check(FromBits(random())); -} - -void ExpectSamePair(const HighLow actual, - const fesa::high_low_original::HighLow expected) { - EXPECT_EQ(Bits(actual.high), Bits(expected.high)); - EXPECT_EQ(Bits(actual.low), Bits(expected.low)); -} - -TEST(HighLowArithmetic, BitExponentMatchesFrozenOracleAcrossEveryExponent) { - ASSERT_TRUE(ArithmeticEnvironmentSupported()); - // Every normal exponent, every possible trailing significand-bit count, - // both signs; then every subnormal single-bit and mixed-bit boundary. - for (std::uint64_t exponent = 1; exponent < 2047; ++exponent) { - for (unsigned trailing = 0; trailing <= 52U; ++trailing) { - const auto fraction = trailing == 52U ? 0ULL : 1ULL << trailing; - for (const auto sign : {0ULL, 1ULL << 63U}) { - const double value = FromBits(sign | (exponent << 52U) | fraction); - ASSERT_EQ(LowestBitExponent(value), - fesa::high_low_original::LowestBitExponent(value)); +namespace { +TEST(HighLowArithmetic, PacketDispatchRetainsComponentsAndInstructionGates) { + using namespace fesa::hex8_internal; + const std::uint32_t leaf1 = (1U << 12) | (1U << 26) | (1U << 27) | (1U << 28); + const std::uint32_t leaf7 = (1U << 3) | (1U << 5) | (1U << 8); + EXPECT_TRUE(SupportsMaterialPacketInstructions(leaf1, 6, leaf7)); + for (const auto bit : {12,26,27,28}) + EXPECT_FALSE(SupportsMaterialPacketInstructions(leaf1 & ~(1U << bit),6,leaf7)); + for (const auto bit : {3,5,8}) + EXPECT_FALSE(SupportsMaterialPacketInstructions(leaf1,6,leaf7 & ~(1U << bit))); + EXPECT_FALSE(SupportsMaterialPacketInstructions(leaf1,2,leaf7)); + EXPECT_FALSE(SupportsMaterialPacketInstructions(leaf1,4,leaf7)); + const auto selected = SelectMaterialPacketEvaluator(); + std::size_t finite_pairs = 0, retained_low_pairs = 0, invalid_pairs = 0; + RecordProperty("selected_backend", selected == EvaluateMaterialPacketAvx2 ? "avx2" : "scalar"); + const auto verify = [selected, &finite_pairs, &retained_low_pairs, &invalid_pairs]( + const MaterialPacketInput& input) { + PairPacket expected{}, actual{}; + EvaluateMaterialPacketScalar(input, &expected); + selected(input, &actual); + for (std::size_t lane = 0; lane < 4; ++lane) { + if (std::isnan(expected.values[lane].high)) { + ++invalid_pairs; + EXPECT_TRUE(std::isnan(actual.values[lane].high)); + } else { + ++finite_pairs; + if (expected.values[lane].low != 0) ++retained_low_pairs; + EXPECT_DOUBLE_EQ(actual.values[lane].high, expected.values[lane].high); + EXPECT_DOUBLE_EQ(actual.values[lane].low, expected.values[lane].low); + EXPECT_EQ(std::signbit(actual.values[lane].low), + std::signbit(expected.values[lane].low)); } } - } - for (unsigned bit = 0; bit < 52U; ++bit) { - for (const auto significand : {1ULL << bit, (1ULL << 52U) - (1ULL << bit)}) { - const double value = FromBits(significand); - EXPECT_EQ(LowestBitExponent(value), - fesa::high_low_original::LowestBitExponent(value)); - } - } -} - -TEST(HighLowArithmetic, OptimizedArithmeticMatchesFrozenOriginalBits) { - ASSERT_TRUE(ArithmeticEnvironmentSupported()); - std::vector values{0.0, -0.0, - std::numeric_limits::denorm_min(), - -std::numeric_limits::denorm_min(), - std::numeric_limits::min(), - std::nextafter(std::numeric_limits::min(), 0.0), - std::numeric_limits::max(), - -std::numeric_limits::max(), 1.0, -1.0, - std::nextafter(1.0, 2.0), std::ldexp(1.0, -53), - std::ldexp(1.0, -54), std::numeric_limits::infinity(), - -std::numeric_limits::infinity(), - std::numeric_limits::quiet_NaN()}; - std::mt19937_64 random{0x58a0fU}; - for (int sample = 0; sample < 4000; ++sample) { - values.push_back(FromBits(random())); - } - for (std::size_t index = 0; index < values.size(); ++index) { - const double high = values[index]; - const double low = values[(index * 13U + 7U) % values.size()]; - const fesa::high_low_original::HighLow original{high, low}; - EXPECT_EQ(IsNormalized({high, low}), - fesa::high_low_original::IsNormalized(original)); - // Normalize a raw pair with the frozen operation, never the new predicate. - const auto a = fesa::high_low_original::TwoSum(high, low); - const auto b = fesa::high_low_original::TwoSum( - values[(index + 1U) % values.size()], - values[(index * 3U + 2U) % values.size()]); - ExpectSamePair(Add({a.high, a.low}, {b.high, b.low}), - fesa::high_low_original::Add(a, b)); - ExpectSamePair(Multiply({a.high, a.low}, {b.high, b.low}), - fesa::high_low_original::Multiply(a, b)); - ExpectSamePair(Divide({a.high, a.low}, {b.high, b.low}), - fesa::high_low_original::Divide(a, b)); - ExpectSamePair(TwoProduct(high, low), - fesa::high_low_original::TwoProduct(high, low)); - } - for (double high : {1.0, std::nextafter(1.0, 2.0), -1.0, - std::numeric_limits::max()}) { - for (double low : {0.0, -0.0, std::ldexp(1.0, -53), - -std::ldexp(1.0, -53)}) { - EXPECT_EQ(IsNormalized({high, low}), - fesa::high_low_original::IsNormalized({high, low})); + }; + for (const bool full_low : {false,true}) + for (const int exponent : {-970,-100,0,100,400}) { + MaterialPacketInput input{}; + input.weight = {0.5, 0}; + for (std::size_t lane = 0; lane < 4; ++lane) { + input.deviatoric_left.values[lane] = + {std::ldexp(1.0, exponent), full_low ? std::ldexp(1.0,exponent-54) : 0}; + input.deviatoric_right.values[lane] = {1.25, std::ldexp(1.0,-60)}; + input.shear_left.values[lane] = {0, 0}; + input.shear_right.values[lane] = {1,0}; + input.tensor_left.values[lane] = {-1, std::ldexp(1.0,-55)}; + input.gradient_dot.values[lane] = {std::ldexp(1.0,exponent),0}; + input.accumulated.values[lane] = {std::ldexp(1.0,exponent),0}; } + verify(input); + // Mixed-low packets explicitly exercise the scalar bridge. + input.deviatoric_left.values[1].low = 0; + verify(input); + input.shear_left.values[3] = {-0.0,0}; // raw signed-zero is invalid by contract + input.deviatoric_left.values[0] = {std::numeric_limits::max(),0}; + input.deviatoric_right.values[0] = {2,0}; + input.deviatoric_left.values[1] = {1,1}; + input.deviatoric_left.values[2] = {std::numeric_limits::denorm_min(),0}; + input.deviatoric_right.values[2] = {0.5,0}; + verify(input); } + EXPECT_GT(finite_pairs, 0U); + EXPECT_GT(retained_low_pairs, 0U); + EXPECT_GT(invalid_pairs, 0U); + RecordProperty("finite_pairs", static_cast(finite_pairs)); + RecordProperty("retained_low_pairs", static_cast(retained_low_pairs)); + RecordProperty("invalid_pairs", static_cast(invalid_pairs)); + // Selection is thread-environment dependent rather than cached after first use. +#if defined(_M_X64) || defined(__SSE2__) + const unsigned saved = _mm_getcsr(); + _mm_setcsr(saved | 0x8000U); + EXPECT_EQ(SelectMaterialPacketEvaluator(), EvaluateMaterialPacketScalar); + _mm_setcsr(saved); + EXPECT_EQ(SelectMaterialPacketEvaluator(), selected); +#endif } } // namespace diff --git a/tests/unit/math/matrix_test.cpp b/tests/unit/math/matrix_test.cpp index b315b44..e6fe177 100644 --- a/tests/unit/math/matrix_test.cpp +++ b/tests/unit/math/matrix_test.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -109,22 +110,60 @@ TEST(DenseMath, RowMajorMatrixMatchesKnownGemvGemm) { std::invalid_argument); } -TEST(DenseMath, MatrixProductsRejectNonfiniteOperandsAndPreserveValidReuse) { - Matrix left{2U, 2U, 1.0}; - Matrix right{2U, 1U, 2.0}; - Vector rhs{2U, 2.0}; - rhs[1U] = std::numeric_limits::infinity(); - EXPECT_THROW(static_cast(left.Multiply(rhs)), std::invalid_argument); - rhs[1U] = 2.0; - left(0U, 1U) = std::numeric_limits::quiet_NaN(); - EXPECT_THROW(static_cast(left.Multiply(rhs)), std::invalid_argument); - EXPECT_THROW(static_cast(left.Multiply(right)), std::invalid_argument); - left(0U, 1U) = 1.0; - right(1U, 0U) = std::numeric_limits::infinity(); - EXPECT_THROW(static_cast(left.Multiply(right)), std::invalid_argument); - right(1U, 0U) = 2.0; - EXPECT_DOUBLE_EQ(left.Multiply(rhs)[0U], 4.0); - EXPECT_DOUBLE_EQ(left.Multiply(right)(1U, 0U), 4.0); + +TEST(DenseMath, MatrixContiguousScaleAndRobustNorm) { + Matrix matrix{2U, 3U}; + const double entries[]{1.0, -2.0, 3.0, -4.0, 5.0, -6.0}; + for (std::size_t i = 0; i < 6U; ++i) matrix.Data()[i] = entries[i]; + const Matrix& immutable = matrix; + EXPECT_EQ(immutable.Data() + 3, &immutable(1U, 0U)); + EXPECT_NEAR(matrix.Norm(), std::sqrt(91.0), 2.0e-15); + matrix.Scale(-2.0); + for (std::size_t i = 0; i < 6U; ++i) + EXPECT_DOUBLE_EQ(matrix.Data()[i], -2.0 * entries[i]); + EXPECT_DOUBLE_EQ(immutable(1U, 2U), 12.0); + Matrix huge{1U, 2U, 1.0e308}; + EXPECT_NEAR(huge.Norm() / 1.0e308, std::sqrt(2.0), 4.0e-16); + Matrix tiny{2U, 1U, 1.0e-300}; + EXPECT_NEAR(tiny.Norm() / 1.0e-300, std::sqrt(2.0), 4.0e-16); + Matrix empty{0U, (std::numeric_limits::max)()}; + EXPECT_DOUBLE_EQ(empty.Norm(), 0.0); + EXPECT_NO_THROW(empty.Scale(3.0)); +} + +TEST(DenseMath, MatrixProductKeepsShapeAndFiniteValidationOrder) { + const double nan = (std::numeric_limits::quiet_NaN)(); + Matrix matrix{1U, 2U, nan}; + try { + static_cast(matrix.Multiply(Vector{1U})); + FAIL() << "Shape mismatch was accepted"; + } catch (const std::invalid_argument& failure) { + EXPECT_STREQ(failure.what(), + "Matrix-vector multiplication has incompatible dimensions."); + } + EXPECT_THROW(static_cast(matrix.Multiply(Vector{2U})), + std::invalid_argument); + EXPECT_THROW(static_cast(matrix.Multiply(Matrix{2U, 0U})), + std::invalid_argument); + EXPECT_THROW(static_cast(Matrix{0U, 2U}.Multiply(Vector{2U, nan})), + std::invalid_argument); + const Matrix finite{2U, 2U, 3.0}; + const auto square = finite.Multiply(finite); + EXPECT_DOUBLE_EQ(square(0U, 0U), 18.0); + EXPECT_DOUBLE_EQ(finite(0U, 0U), 3.0); +} + +TEST(DenseMath, MatrixSelfAssignmentPreservesShapeAndStorage) { + Matrix matrix{2U, 1U, 7.0}; + const Matrix* same = &matrix; + matrix = *same; + EXPECT_EQ(matrix.Rows(), 2U); + EXPECT_EQ(matrix.Columns(), 1U); + EXPECT_DOUBLE_EQ(matrix(1U, 0U), 7.0); + Matrix* mutable_same = &matrix; + matrix = std::move(*mutable_same); + EXPECT_EQ(matrix.Rows(), 2U); + EXPECT_DOUBLE_EQ(matrix(1U, 0U), 7.0); } } // namespace diff --git a/tests/unit/math/sparse_matrix_test.cpp b/tests/unit/math/sparse_matrix_test.cpp index 7185f91..7754a2f 100644 --- a/tests/unit/math/sparse_matrix_test.cpp +++ b/tests/unit/math/sparse_matrix_test.cpp @@ -3,8 +3,6 @@ #include #include -#include -#include #include #include #include @@ -133,104 +131,4 @@ TEST(SparseAssembly, PreservesExpectedStructuralZeros) { 4); } -TEST(SparseAssembly, MultipliesRectangularCopyMoveAndConcurrentConstCalls) { - auto assembled = SparseMatrix::FromCoo( - 3U, 5U, {{0U, 4U, 3.0, 0U, 0U}, {2U, 1U, -2.0, 1U, 0U}}, - {{0U, 1U, 1U, 2U}, {4U, 1U}}); - ASSERT_TRUE(assembled.HasValue()); - const auto original = assembled.Value(); - auto copied = original; - auto moved = std::move(copied); - Vector rhs{5U, 2.0}; - const auto evaluate = [&rhs](const SparseMatrix& matrix) { - for (int repeat = 0; repeat < 20; ++repeat) { - const auto product = matrix.Multiply(rhs); - if (product.Size() != 3U || product[0U] != 6.0 || - product[1U] != 0.0 || product[2U] != -4.0) return false; - } - return true; - }; - auto first = std::async(std::launch::async, [&] { return evaluate(moved); }); - auto second = std::async(std::launch::async, [&] { return evaluate(moved); }); - EXPECT_TRUE(first.get()); - EXPECT_TRUE(second.get()); - EXPECT_TRUE(evaluate(original)); - auto replacement = SparseMatrix::FromCoo( - 1U, 1U, {{0U, 0U, 7.0, 0U, 0U}}, {{0U, 1U}, {0U}}); - ASSERT_TRUE(replacement.HasValue()); - moved = replacement.Value(); - EXPECT_DOUBLE_EQ(moved.Multiply(Vector{1U, 3.0})[0U], 21.0); - EXPECT_TRUE(evaluate(original)); -} - -TEST(SparseAssembly, PreservesZeroShapesAndEmptyRows) { - auto no_rows = SparseMatrix::FromCoo(0U, 3U, {}, {{0U}, {}}); - auto no_columns = SparseMatrix::FromCoo(2U, 0U, {}, {{0U, 0U, 0U}, {}}); - auto no_entries = SparseMatrix::FromCoo(2U, 3U, {}, {{0U, 0U, 0U}, {}}); - ASSERT_TRUE(no_rows.HasValue()); - ASSERT_TRUE(no_columns.HasValue()); - ASSERT_TRUE(no_entries.HasValue()); - EXPECT_EQ(no_rows.Value().Multiply(Vector{3U}).Size(), 0U); - const auto empty_columns = no_columns.Value().Multiply(Vector{0U}); - const auto empty_entries = no_entries.Value().Multiply(Vector{3U}); - ASSERT_EQ(empty_columns.Size(), 2U); - ASSERT_EQ(empty_entries.Size(), 2U); - for (std::size_t row = 0; row < 2U; ++row) { - EXPECT_DOUBLE_EQ(empty_columns[row], 0.0); - EXPECT_DOUBLE_EQ(empty_entries[row], 0.0); - } -} - -TEST(SparseAssembly, RejectsNonfiniteRhsAndRecoversAfterFiniteOverflow) { - auto matrix = SparseMatrix::FromCoo( - 2U, 2U, {{0U, 0U, 2.0, 0U, 0U}}, {{0U, 1U, 2U}, {0U, 1U}}); - ASSERT_TRUE(matrix.HasValue()); - Vector rhs{2U, (std::numeric_limits::infinity)()}; - EXPECT_THROW(static_cast(matrix.Value().Multiply(rhs)), - std::invalid_argument); - rhs[1U] = 4.0; - rhs[0U] = (std::numeric_limits::max)(); - EXPECT_TRUE(std::isinf(matrix.Value().Multiply(rhs)[0U])); - rhs[0U] = (std::numeric_limits::quiet_NaN)(); - EXPECT_THROW(static_cast(matrix.Value().Multiply(rhs)), - std::invalid_argument); - EXPECT_THROW(static_cast(matrix.Value().Multiply(Vector{1U})), - std::invalid_argument); - rhs[0U] = 3.0; - rhs[1U] = 4.0; - const auto finite = matrix.Value().Multiply(rhs); - EXPECT_DOUBLE_EQ(finite[0U], 6.0); - EXPECT_DOUBLE_EQ(finite[1U], 0.0); -} - -TEST(SparseAssembly, MatchesIndependentLongDoubleResidualOnUnevenRows) { - const std::vector entries{1.25, -0.375, 4.5, -3.0, 0.125, 2.0}; - const SparsePattern pattern{{0U, 3U, 3U, 5U, 6U}, {0U, 2U, 4U, 1U, 3U, 2U}}; - std::vector contributions; - for (std::size_t row = 0; row < 4U; ++row) { - for (auto position = pattern.row_offsets[row]; - position < pattern.row_offsets[row + 1U]; ++position) { - contributions.push_back( - {row, pattern.column_indices[position], entries[position], row, - position}); - } - } - auto matrix = SparseMatrix::FromCoo(4U, 5U, contributions, pattern); - ASSERT_TRUE(matrix.HasValue()); - Vector rhs{5U}; - for (std::size_t column = 0; column < 5U; ++column) { - rhs[column] = 0.3 + static_cast(column) * 0.7; - } - const auto product = matrix.Value().Multiply(rhs); - for (std::size_t row = 0; row < 4U; ++row) { - long double expected = 0; - for (auto position = pattern.row_offsets[row]; - position < pattern.row_offsets[row + 1U]; ++position) { - expected += static_cast(entries[position]) * - rhs[pattern.column_indices[position]]; - } - EXPECT_NEAR(product[row], static_cast(expected), 2.0e-14); - } -} - } // namespace diff --git a/tests/unit/math/vector3_test.cpp b/tests/unit/math/vector3_test.cpp index bd51337..48d2d64 100644 --- a/tests/unit/math/vector3_test.cpp +++ b/tests/unit/math/vector3_test.cpp @@ -112,5 +112,19 @@ TEST(Vector3, RejectsZeroAndNonfiniteNormalization) { EXPECT_FALSE(Vector3(0.0, not_a_number, 0.0).Normalized().has_value()); } + +TEST(Vector3, RightHandedFrameMatchesIndependentCoordinateTransform) { + const Vector3 x_axis{0.0, 1.0, 0.0}; + const Vector3 y_axis{0.0, 0.0, 1.0}; + const Vector3 z_axis = x_axis.Cross(y_axis); + const Vector3 global{2.0, -3.0, 4.0}; + EXPECT_DOUBLE_EQ(x_axis.Dot(global), -3.0); + EXPECT_DOUBLE_EQ(y_axis.Dot(global), 4.0); + EXPECT_DOUBLE_EQ(z_axis.Dot(global), 2.0); + const Vector3 recovered = x_axis * -3.0 + y_axis * 4.0 + z_axis * 2.0; + EXPECT_TRUE(recovered == global); + EXPECT_DOUBLE_EQ(x_axis.Dot(y_axis.Cross(z_axis)), 1.0); +} + } // namespace } // namespace fesa diff --git a/tests/unit/math/vector_test.cpp b/tests/unit/math/vector_test.cpp index bd62869..59d221b 100644 --- a/tests/unit/math/vector_test.cpp +++ b/tests/unit/math/vector_test.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -83,5 +84,31 @@ TEST(DenseMath, VectorOwnsAndChecksContiguousStorage) { EXPECT_THROW(original.Axpy(1.0, Vector{2}), std::invalid_argument); } + +TEST(DenseMath, VectorRobustNormSelfAliasAndNonfinitePropagation) { + Vector large{2U, 1.0e308}; + EXPECT_NEAR(large.Norm() / 1.0e308, std::sqrt(2.0), 4.0e-16); + Vector tiny{2U, 1.0e-300}; + EXPECT_NEAR(tiny.Norm() / 1.0e-300, std::sqrt(2.0), 4.0e-16); + Vector values{2U}; + values[0] = 2.0; + values[1] = -3.0; + values.Axpy(-2.0, values); + EXPECT_DOUBLE_EQ(values[0], -2.0); + EXPECT_DOUBLE_EQ(values[1], 3.0); + const Vector* same = &values; + values = *same; + Vector* mutable_same = &values; + values = std::move(*mutable_same); + EXPECT_DOUBLE_EQ(values.Dot(values), 13.0); + Vector nonfinite{1U, (std::numeric_limits::quiet_NaN)()}; + EXPECT_TRUE(std::isnan(nonfinite.Dot(Vector{1U, 1.0}))); + EXPECT_TRUE(std::isnan(nonfinite.Norm())); + EXPECT_NO_THROW(nonfinite.Scale(2.0)); + EXPECT_NO_THROW(nonfinite.Axpy(1.0, Vector{1U, 1.0})); + EXPECT_TRUE(std::isnan(nonfinite[0])); + EXPECT_DOUBLE_EQ(Vector{0U}.Dot(Vector{0U}), 0.0); +} + } // namespace } // namespace fesa diff --git a/tests/unit/model/shell_geometry_test.cpp b/tests/unit/model/shell_geometry_test.cpp index 41ab691..460b716 100644 --- a/tests/unit/model/shell_geometry_test.cpp +++ b/tests/unit/model/shell_geometry_test.cpp @@ -283,3 +283,14 @@ TEST(Mitc4Geometry, ExposesTheCompleteRequiredValidationPointInventory) { EXPECT_EQ(points[13].natural_coordinates, (Vector3{-gauss, -gauss, 0.0})); EXPECT_EQ(points[16].natural_coordinates, (Vector3{-gauss, gauss, 0.0})); } + +TEST(Mitc4Geometry, RetainsReciprocalDeterminantOverflowGateAtTinyScale) { + const std::vector nodes{ + Node(0U, {0.0, 0.0, 0.0}), Node(1U, {2e-104, 0.0, 0.0}), + Node(2U, {2e-104, 2e-104, 0.0}), Node(3U, {0.0, 2e-104, 0.0})}; + EXPECT_TRUE(fesa::PreprocessShellGeometry( + nodes, {Element(10U, {0U, 1U, 2U, 3U})}, Sections(2e-99)).HasValue()); + ExpectFailureCode(fesa::PreprocessShellGeometry( + nodes, {Element(10U, {0U, 1U, 2U, 3U})}, Sections(2e-102)), + "invalid-shell-jacobian"); +} diff --git a/tests/unit/model/source_target_resolver_test.cpp b/tests/unit/model/source_target_resolver_test.cpp index fc94cb8..1aa6273 100644 --- a/tests/unit/model/source_target_resolver_test.cpp +++ b/tests/unit/model/source_target_resolver_test.cpp @@ -133,3 +133,41 @@ TEST(SourceTargetResolver, EXPECT_EQ(result.GetStatus().Diagnostics()[1U].message, "Duplicate resolved source target 10."); } + +TEST(SourceTargetResolver, OwnsNodeCandidateIndexBeforeDomainCommit) { + using fesa::SourceEntityKind; + const auto index = [] { + const std::vector nodes{{{"Part-1", 20, "0020"}, {}, {}}, + {{"Part-1", 10, "0010"}, {}, {}}, + {{"Part-2", 10, "0010"}, {}, {}}}; + const std::vector sets{ + {"Ends", "Part-1", {1U, 0U}, {}}, + {"Ends", "Part-2", {2U}, {}}, + {"Repeated", "Part-1", {0U, 1U, 0U, 1U}, {}}}; + return fesa::SourceTargetIndex::FromNodeDefinitions(nodes, sets); + }(); + const fesa::SourceTargetResolver resolver{index}; + const auto ends = + resolver.Resolve({SourceEntityKind::kNode, "part-1", "ENDS"}); + ASSERT_TRUE(ends.HasValue()); + EXPECT_EQ(Indices(ends.Value()), (std::vector{1U, 0U})); + EXPECT_EQ(ends.Value()[0U].source_id.source_label_text, "0010"); + EXPECT_EQ(ends.Value()[1U].source_id.source_label_text, "0020"); + const auto direct = + resolver.Resolve({SourceEntityKind::kNode, "Part-2", "10"}); + ASSERT_TRUE(direct.HasValue()); + EXPECT_EQ(Indices(direct.Value()), (std::vector{2U})); + ExpectFailure(index, {SourceEntityKind::kNode, "", "Ends"}, + "ambiguous-source-target"); + const auto repeated = + resolver.Resolve({SourceEntityKind::kNode, "Part-1", "Repeated"}); + ASSERT_FALSE(repeated.HasValue()); + ASSERT_EQ(repeated.GetStatus().Diagnostics().size(), 2U); + EXPECT_EQ(repeated.GetStatus().Diagnostics()[0U].message, + "Duplicate resolved source target 0020."); + EXPECT_EQ(repeated.GetStatus().Diagnostics()[1U].message, + "Duplicate resolved source target 0010."); + const auto empty = fesa::SourceTargetIndex::FromNodeDefinitions({}, {}); + ExpectFailure(empty, {SourceEntityKind::kNode, "", "1"}, + "unresolved-source-target"); +} diff --git a/tests/unit/model/thermal_shell_geometry_test.cpp b/tests/unit/model/thermal_shell_geometry_test.cpp index 4c7633d..6988e65 100644 --- a/tests/unit/model/thermal_shell_geometry_test.cpp +++ b/tests/unit/model/thermal_shell_geometry_test.cpp @@ -7,6 +7,7 @@ #include "fesa/elements/thermal_shell.h" #include "fesa/model/domain.h" +#include "elements/thermal_shell_geometry.h" namespace { using fesa::Vector3; @@ -210,3 +211,68 @@ TEST(ThermalShellGeometry, {1, 1, 0}}, {{{0, 1, 4, 3}}, {{1, 2, 5, 4}}, {{3, 4, 7, 6}}, {{4, 5, 8, 7}}}))); } + +namespace { +TEST(ThermalGeometryAction, EnclosesIndependentDyadicTangentialConstraints) { + using namespace fesa::thermal_internal; + // The rounded Gram matrix loses rank for delta=2^-40, while the physical + // tangents and their exterior determinant remain certified and nonsingular. + for (const int exponent : {-40,-27,-4,0}) { + const double delta = std::ldexp(1.,exponent); + Geometry geometry; + geometry.b = {1,1,0}; + geometry.c = {1,1+delta,0}; + for (std::size_t j=0; j<3; ++j) { + geometry.bi[j] = Interval{geometry.b[j]}; + geometry.ci[j] = Interval{geometry.c[j]}; + } + const auto evaluated = EvaluateSurface(geometry, 0, 0, Interval{0}, + Interval{0}, {}, {}); + ASSERT_TRUE(evaluated.HasValue()) << exponent; + const double dx[4]{-.25,.25,.25,-.25}; + const double dy[4]{-.25,-.25,.25,.25}; + for (std::size_t i=0; i<4; ++i) { + const double gy = (dy[i]-dx[i])/delta; + const double gx = dx[i]-gy; + const double oracle[3]{gx,gy,0}; + for (std::size_t j=0; j<3; ++j) { + EXPECT_DOUBLE_EQ(evaluated.Value().gradient[i][j],oracle[j]); + const auto interval = evaluated.Value().gradient_interval[i][j]; + EXPECT_LE(interval.lower,oracle[j]); + EXPECT_GE(interval.upper,oracle[j]); + } + } + } +} +TEST(ThermalGeometryAction, PreservesWholePatchAndScaledFiniteRejections) { + using namespace fesa::thermal_internal; + for (const Patch patch : { + kSquare, + Patch{{{3,-9,-3},{1,3,-3},{-1,3,3},{-3,3,3}}}, + Patch{{{-1,4,-4},{-1,-2,2},{3,2,4},{-1,-4,-2}}}}) { + const auto geometry = BuildGeometry(patch, {}, {}); + ASSERT_TRUE(geometry.HasValue()); + for (const double xi : {-std::sqrt(.6),0.,std::sqrt(.6)}) + for (const double eta : {-std::sqrt(.6),0.,std::sqrt(.6)}) + EXPECT_TRUE(EvaluateSurface(geometry.Value(),xi,eta, + Interval{Down(xi),Up(xi)},Interval{Down(eta),Up(eta)},{},{}).HasValue()); + } + const Patch folded{{{0,0,0},{1,1,0},{1,0,0},{0,1,0}}}; + const auto rejected = BuildGeometry(folded,{"part",27,"0027"},{"mesh.inp",17}); + ASSERT_FALSE(rejected.HasValue()); + EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].code, + "uncertain-thermal-shell-geometry"); + EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].entity_identity,"part:0027"); + Geometry geometry; + geometry.b = {1,0,0}; geometry.c = {0,1,0}; + geometry.bi[0] = Interval{1}; geometry.ci[1] = Interval{1}; + geometry.length_exponent = -600; + const auto scaled = EvaluateSurface(geometry,0,0,Interval{0},Interval{0}, + {"part",27,"0027"},{"mesh.inp",17}); + ASSERT_FALSE(scaled.HasValue()); + EXPECT_EQ(scaled.GetStatus().Diagnostics()[0].code, + "uncertain-thermal-shell-metric"); + EXPECT_EQ(scaled.GetStatus().Diagnostics()[0].message, + "Metric inverse enclosure is not finite or excludes no zero divisor."); +} +} // namespace diff --git a/tests/unit/results/result_records_test.cpp b/tests/unit/results/result_records_test.cpp index a418ddf..c35285c 100644 --- a/tests/unit/results/result_records_test.cpp +++ b/tests/unit/results/result_records_test.cpp @@ -9,6 +9,9 @@ #include "fesa/analysis/analysis_state.h" +#include "fesa/analysis/analysis_model.h" +#include "fesa/fem/dof_manager.h" + namespace { fesa::DofManager MakeEmptyDofs() { diff --git a/tests/unit/results/result_recovery_components_test.cpp b/tests/unit/results/result_recovery_components_test.cpp index 1f9a057..91bcc79 100644 --- a/tests/unit/results/result_recovery_components_test.cpp +++ b/tests/unit/results/result_recovery_components_test.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -354,41 +353,6 @@ TEST(ResultRecoveryComponents, } } -TEST(ResultRecoveryComponents, PartitionedScaleKeepsCancelledInternalTerms) { - auto definition = MakeShellDefinition(); - definition.node_sets = {{"Fixed", std::optional{"Shell-1"}, - {0U}, {definition.source_path, 60U}}}; - definition.steps[0].boundaries = { - {"Fixed", 1, 1, 1.0, {definition.source_path, 61U}}}; - const auto fixture = MakeShellFixture(std::move(definition)); - const auto count = fixture.dofs->FullDofCount(); - fesa::SparsePattern pattern; - pattern.row_offsets = {0U, 1U, 3U}; - pattern.row_offsets.resize(count + 1U, 3U); - pattern.column_indices = {0U, 0U, 1U}; - auto matrix = fesa::SparseMatrix::FromCoo( - count, count, - {{0U, 0U, 1.0, 0U, 0U}, {1U, 0U, -1.0, 0U, 1U}, - {1U, 1U, 1.0, 0U, 2U}}, pattern); - ASSERT_TRUE(matrix.HasValue()); - fesa::Vector displacement{count}; - displacement[0U] = 1.0; - displacement[1U] = 1.0; - fesa::Vector external{count}; - external[1U] = 1.0e-12; - const auto result = fesa::results_internal::GlobalEquilibriumRecovery( - *fixture.domain, *fixture.dofs, matrix.Value(), displacement, external); - ASSERT_TRUE(result.HasValue()); - EXPECT_DOUBLE_EQ(result.Value().internal_force[1U], 0.0); - EXPECT_DOUBLE_EQ(result.Value().normalized_free_residual, 1.0e-12); - displacement[1U] = std::numeric_limits::quiet_NaN(); - const auto rejected = fesa::results_internal::GlobalEquilibriumRecovery( - *fixture.domain, *fixture.dofs, matrix.Value(), displacement, external); - ASSERT_FALSE(rejected.HasValue()); - ExpectStatusCode(rejected.GetStatus(), "nonfinite-recovery-value"); - EXPECT_DOUBLE_EQ(result.Value().reaction[1U], -1.0e-12); -} - // C-MODULE-002 TEST(ResultRecoveryComponents, BeamResultRecoveryKeepsEndpointGaussAndStressIdentities) { diff --git a/tests/unit/results/results_writer_test.cpp b/tests/unit/results/results_writer_test.cpp index b2dceb8..a4842c7 100644 --- a/tests/unit/results/results_writer_test.cpp +++ b/tests/unit/results/results_writer_test.cpp @@ -1,9 +1,22 @@ #include "fesa/results/results_writer.h" +#include "fesa/results/prepared_results_writer.h" +#include "fesa/results/dynamic_results_writer.h" +#include "fesa/results/eigen_results_writer.h" +#include "fesa/results/nonlinear_results_writer.h" +#include "fesa/results/solid_results_writer.h" +#include "fesa/results/thermal_results_writer.h" + +#include + +#include #include +#include +#include #include #include +#include "fesa/analysis/analysis_model.h" #include "fesa/analysis/analysis_state.h" #include "fesa/core/diagnostic.h" #include "fesa/core/status.h" @@ -21,3 +34,181 @@ static_assert( std::is_same_v); } // namespace + +namespace { + +// These adapters only observe borrowed values; no procedure, kernel, or file I/O. +struct WriterCalls { + std::vector paths; + std::vector domains; + std::vector*> diagnostics; + std::vector families; + + fesa::Status Record(unsigned family, const std::filesystem::path& path, + const fesa::Domain& domain, + const std::vector& values) { + paths.push_back(&path); + domains.push_back(&domain); + diagnostics.push_back(&values); + families.push_back(family); + return fesa::Status::Failure( + fesa::FailureCategory::kOutput, + {{fesa::Severity::kError, "candidate-close-failure", {"source.inp", 7U}, + "WRITER", "0007", "Preserved writer failure."}}); + } +}; + +template +class RecordingWriter final : public Writer { + public: + explicit RecordingWriter(WriterCalls& calls) : calls_{calls} {} + fesa::Status Write(const std::filesystem::path& path, + const fesa::Domain& domain, const State& state, + const std::vector& diagnostics) override { + received = &state; + return calls_.Record(Family, path, domain, diagnostics); + } + const State* received{nullptr}; + + private: + WriterCalls& calls_; +}; + +class RecordingSolidWriter final : public fesa::SolidResultsWriter { + public: + explicit RecordingSolidWriter(WriterCalls& calls) : calls_{calls} {} + fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d, + const fesa::SolidStaticState& s, + const std::vector& v) override { + static_state = &s; + return calls_.Record(5U, p, d, v); + } + fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d, + const fesa::SolidFrequencyState& s, + const std::vector& v) override { + frequency_state = &s; + return calls_.Record(6U, p, d, v); + } + fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d, + const fesa::SolidDynamicAnalysisState& s, + const std::vector& v) override { + dynamic_state = &s; + return calls_.Record(7U, p, d, v); + } + const fesa::SolidStaticState* static_state{nullptr}; + const fesa::SolidFrequencyState* frequency_state{nullptr}; + const fesa::SolidDynamicAnalysisState* dynamic_state{nullptr}; + + private: + WriterCalls& calls_; +}; + +// Empty values intentionally bypass physics: dispatch must preserve their type. +std::vector EmptyOutcomes() { + std::vector values; + values.emplace_back(fesa::LinearStaticExecutionResult{ + fesa::AnalysisState::Create(fesa::DofManager{}, {"source/step", 9U}), {}}); + fesa::ModelDefinition definition{}; + definition.steps = {{"value-only", {}, {}, 0.1, 1.0, 0.01, 1.0, {}}}; + auto domain = fesa::Domain::Create(std::move(definition)); + auto model = fesa::AnalysisModel::Create(domain.Value()); + auto dofs = fesa::DofManager::Create(model.Value()); + auto nonlinear = fesa::NonlinearAnalysisState::Create( + domain.Value(), dofs.Value(), {}); + if (!nonlinear.HasValue()) + throw std::runtime_error{"Empty nonlinear value construction failed."}; + values.emplace_back( + fesa::NonlinearStaticExecutionResult{std::move(nonlinear.Value()), {}}); + values.emplace_back(fesa::EigenAnalysisState{ + fesa::FeastEigenResult{0U, {}, fesa::Matrix{0U, 0U}}}); + values.emplace_back(fesa::DynamicExecutionResult{}); + values.emplace_back(fesa::SolidStaticExecutionResult{}); + values.emplace_back(fesa::SolidFrequencyState{ + fesa::FeastEigenResult{0U, {}, fesa::Matrix{0U, 0U}}}); + values.emplace_back(fesa::SolidDynamicExecutionResult{}); + return values; +} + +} // namespace + +TEST(PreparedResultsWriter, RoutesValueOutcomesAndPreservesWriterFailure) { + auto domain = fesa::Domain::Create({}); + ASSERT_TRUE(domain.HasValue()); + const std::filesystem::path output{"relative/custom-result.h5"}; + const std::vector diagnostics{ + {fesa::Severity::kWarning, "original-warning", {"source.inp", 3U}, + "NODE", "0003", "Original diagnostic."}}; + WriterCalls calls; + RecordingWriter linear{calls}; + RecordingWriter + nonlinear{calls}; + RecordingWriter + eigen{calls}; + RecordingWriter + dynamic{calls}; + RecordingWriter + thermal{calls}; + RecordingSolidWriter solid{calls}; + auto outcomes = EmptyOutcomes(); + const std::array families{0U, 1U, 2U, 3U, 5U, 6U, 7U}; + ASSERT_EQ(outcomes.size(), families.size()); + for (std::size_t index = 0U; index < outcomes.size(); ++index) { + const auto status = fesa::WritePreparedAnalysisResult( + output, domain.Value(), outcomes[index], diagnostics, linear, + nonlinear, eigen, &dynamic, &thermal, &solid); + ASSERT_EQ(calls.families.size(), index + 1U); + EXPECT_EQ(calls.families.back(), families[index]); + EXPECT_EQ(calls.paths.back(), &output); + EXPECT_EQ(calls.domains.back(), &domain.Value()); + EXPECT_EQ(calls.diagnostics.back(), &diagnostics); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); + ASSERT_EQ(status.Diagnostics().size(), 1U); + EXPECT_EQ(status.Diagnostics()[0U].code, "candidate-close-failure"); + EXPECT_EQ(status.Diagnostics()[0U].location.file, "source.inp"); + EXPECT_EQ(status.Diagnostics()[0U].location.line, 7U); + EXPECT_EQ(status.Diagnostics()[0U].keyword, "WRITER"); + EXPECT_EQ(status.Diagnostics()[0U].entity_identity, "0007"); + EXPECT_EQ(status.Diagnostics()[0U].message, "Preserved writer failure."); + } + EXPECT_EQ(linear.received, + &std::get(outcomes[0U]).state); + EXPECT_EQ(nonlinear.received, + &std::get(outcomes[1U]).state); + EXPECT_EQ(eigen.received, &std::get(outcomes[2U])); + EXPECT_EQ(dynamic.received, + &std::get(outcomes[3U]).state); + EXPECT_EQ(thermal.received, nullptr); + EXPECT_EQ(solid.static_state, + &std::get(outcomes[4U]).state); + EXPECT_EQ(solid.frequency_state, + &std::get(outcomes[5U])); + EXPECT_EQ(solid.dynamic_state, + &std::get(outcomes[6U]).state); +} + +TEST(PreparedResultsWriter, MissingOptionalAdapterInvokesNoOtherWriter) { + auto domain = fesa::Domain::Create({}); + ASSERT_TRUE(domain.HasValue()); + WriterCalls calls; + RecordingWriter linear{calls}; + RecordingWriter + nonlinear{calls}; + RecordingWriter + eigen{calls}; + auto outcomes = EmptyOutcomes(); + const std::array codes{ + "missing-dynamic-writer", "missing-solid-writer", + "missing-solid-writer", "missing-solid-writer"}; + const std::array keywords{"DYNAMIC", "", "", ""}; + for (std::size_t index = 3U; index < outcomes.size(); ++index) { + const auto status = fesa::WritePreparedAnalysisResult( + "explicit.h5", domain.Value(), outcomes[index], {}, linear, nonlinear, + eigen); + EXPECT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); + ASSERT_EQ(status.Diagnostics().size(), 1U); + EXPECT_EQ(status.Diagnostics()[0U].code, codes[index - 3U]); + EXPECT_EQ(status.Diagnostics()[0U].keyword, keywords[index - 3U]); + EXPECT_TRUE(calls.families.empty()); + } +} diff --git a/tests/unit/solvers/linear/linear_solver_test.cpp b/tests/unit/solvers/linear/linear_solver_test.cpp index b9b95aa..98025f3 100644 --- a/tests/unit/solvers/linear/linear_solver_test.cpp +++ b/tests/unit/solvers/linear/linear_solver_test.cpp @@ -2,13 +2,22 @@ #include +#include +#include +#include #include #include #include +#include "fesa/analysis/linear_frequency_procedure.h" +#include "fesa/analysis/solid_frequency_procedure.h" +#include "fesa/solvers/eigen/eigen_solver.h" +#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h" #include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" +#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h" +#include "solvers/linear/pardiso_session_test_seam.h" namespace { @@ -125,3 +134,277 @@ TEST(MklPardisoSolver, RejectsInvalidCsrStateAndDimensions) { EXPECT_EQ(no_diagonal_status.Diagnostics().front().code, "solver-missing-diagonal"); } + +namespace { + +class RecordingEigenSolver final : public fesa::EigenSolver { + public: + explicit RecordingEigenSolver(bool& destroyed) : destroyed_(destroyed) {} + ~RecordingEigenSolver() override { destroyed_ = true; } + + fesa::Result Solve( + const fesa::SparseMatrix& matrix, std::size_t modes, + double scale) const override { + EXPECT_EQ(matrix.Rows(), 0U); + EXPECT_EQ(modes, 3U); + EXPECT_DOUBLE_EQ(scale, 7.0); + ++standard_calls; + return fesa::Result::Failure( + fesa::Status::Failure(fesa::FailureCategory::kSolver, {})); + } + + fesa::Result SolveGeneralized( + const fesa::SparseMatrix& stiffness, const fesa::SparseMatrix& mass, + std::size_t modes, double lower_bound, + double scale) const override { + EXPECT_EQ(stiffness.Rows(), 0U); + EXPECT_EQ(mass.Rows(), 0U); + EXPECT_EQ(modes, 4U); + EXPECT_DOUBLE_EQ(lower_bound, 2.0); + EXPECT_DOUBLE_EQ(scale, 11.0); + ++generalized_calls; + return fesa::Result::Failure( + fesa::Status::Failure(fesa::FailureCategory::kSolver, {})); + } + + mutable int standard_calls{0}; + mutable int generalized_calls{0}; + + private: + bool& destroyed_; +}; + +} // namespace + +TEST(EigenSolverContract, DispatchesBothPencilsAndDestroysThroughBase) { + static_assert(std::is_abstract_v); + static_assert(std::has_virtual_destructor_v); + static_assert(std::is_base_of_v); + static_assert(std::is_constructible_v); + static_assert(std::is_constructible_v); + bool destroyed = false; + { + auto recording = std::make_unique(destroyed); + const auto* observations = recording.get(); + std::unique_ptr solver = std::move(recording); + const fesa::EigenSolver& contract = *solver; + const auto empty = MakeDenseCsr(0U, 0U, {}); + EXPECT_FALSE(contract.Solve(empty, 3U, 7.0).HasValue()); + EXPECT_FALSE(contract.SolveGeneralized(empty, empty, 4U, 2.0, 11.0) + .HasValue()); + EXPECT_EQ(observations->standard_calls, 1); + EXPECT_EQ(observations->generalized_calls, 1); + EXPECT_FALSE(destroyed); + } + EXPECT_TRUE(destroyed); +} + +TEST(PardisoSession, RetainsDistinctStoragePoliciesAndFreshPhaseOrder) { + std::vector phases; + fesa::internal::ScopedPardisoBackendTestHook hook{ + [&](const fesa::internal::PardisoBackendTestCall& call, + MKL_INT&, MKL_INT&) { + phases.push_back(static_cast(call.phase)); + if (call.phase == 11) { + EXPECT_EQ(call.equation_count, 2); + if (call.matrix_type == 2) { + EXPECT_EQ(call.row_offsets[2], 3); + EXPECT_EQ(call.column_indices[0], 0); + EXPECT_EQ(call.column_indices[1], 1); + EXPECT_EQ(call.column_indices[2], 1); + } else { + EXPECT_EQ(call.matrix_type, 11); + EXPECT_EQ(call.row_offsets[2], 4); + EXPECT_DOUBLE_EQ(call.values[2], 0.0); + } + } + }}; + { + fesa::MklPardisoSolver solver; + const auto matrix = MakeDenseCsr(2U, 2U, {4.0, 1.0, 1.0, 3.0}); + ASSERT_TRUE(solver.Factorize(matrix).IsOk()); + fesa::Vector solution{2U}; + ASSERT_TRUE(solver.Solve(fesa::Vector{2U, 1.0}, solution).IsOk()); + EXPECT_NEAR(solution[0], 2.0 / 11.0, 1e-14); + ASSERT_TRUE(solver.Solve(fesa::Vector{2U, 2.0}, solution).IsOk()); + EXPECT_NEAR(solution[1], 6.0 / 11.0, 1e-14); + ASSERT_TRUE(solver.Factorize(matrix).IsOk()); + } + EXPECT_EQ(phases, (std::vector{11, 22, 33, 33, -1, 11, 22, -1})); + phases.clear(); + { + fesa::MklPardisoTangentSolver solver; + const auto matrix = MakeDenseCsr(2U, 2U, {2.0, 1.0, 0.0, 3.0}); + ASSERT_TRUE(solver.FactorizeFresh(matrix).IsOk()); + fesa::Vector correction{2U}; + ASSERT_TRUE(solver.SolveCorrection(fesa::Vector{2U, 6.0}, correction).IsOk()); + EXPECT_NEAR(correction[0], 2.0, 1e-14); + EXPECT_NEAR(correction[1], 2.0, 1e-14); + ASSERT_TRUE(solver.FactorizeFresh(matrix).IsOk()); + } + EXPECT_EQ(phases, (std::vector{11, 22, 33, -1, 11, 22, -1})); +} + +TEST(PardisoSession, FailedSolvePreservesOutputAndRetainedFactorization) { + int solve_calls = 0; + fesa::internal::ScopedPardisoBackendTestHook hook{ + [&](const fesa::internal::PardisoBackendTestCall& call, + MKL_INT& error, MKL_INT&) { + if (call.phase != 33) return; + ++solve_calls; + if (solve_calls == 1) error = -3; + if (solve_calls == 2) + call.solution[0] = (std::numeric_limits::infinity)(); + }}; + fesa::MklPardisoSolver solver; + ASSERT_TRUE(solver.Factorize(MakeDenseCsr(1U, 1U, {2.0})).IsOk()); + fesa::Vector solution{1U, 17.0}; + const auto failed = solver.Solve(fesa::Vector{1U, 6.0}, solution); + ASSERT_FALSE(failed.IsOk()); + EXPECT_EQ(failed.Diagnostics().front().code, "pardiso-solve-failed"); + EXPECT_DOUBLE_EQ(solution[0], 17.0); + const auto nonfinite = solver.Solve(fesa::Vector{1U, 6.0}, solution); + ASSERT_FALSE(nonfinite.IsOk()); + EXPECT_EQ(nonfinite.Diagnostics().front().code, "nonfinite-solver-solution"); + EXPECT_DOUBLE_EQ(solution[0], 17.0); + ASSERT_TRUE(solver.Solve(fesa::Vector{1U, 6.0}, solution).IsOk()); + EXPECT_DOUBLE_EQ(solution[0], 3.0); +} + +TEST(PardisoSession, ReleaseFailurePrecedesNewValidationAndInvalidatesState) { + int releases = 0; + fesa::internal::ScopedPardisoBackendTestHook hook{ + [&](const fesa::internal::PardisoBackendTestCall& call, + MKL_INT& error, MKL_INT&) { + if (call.phase == -1) { ++releases; error = -3; } + }}; + fesa::MklPardisoSolver solver; + ASSERT_TRUE(solver.Factorize(MakeDenseCsr(1U, 1U, {2.0})).IsOk()); + const auto status = solver.Factorize(MakeDenseCsr(1U, 2U, {1.0, 2.0})); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Diagnostics().front().code, "pardiso-release-failed"); + fesa::Vector output{1U, 19.0}; + const auto unavailable = solver.Solve(fesa::Vector{1U, 1.0}, output); + ASSERT_FALSE(unavailable.IsOk()); + EXPECT_EQ(unavailable.Diagnostics().front().code, "solver-not-factorized"); + EXPECT_DOUBLE_EQ(output[0], 19.0); + EXPECT_EQ(releases, 1); +} + +TEST(PardisoSession, FactorizationFailuresAndExceptionsReleaseExactlyOnce) { + const auto matrix = MakeDenseCsr(1U, 1U, {2.0}); + for (const int failing_phase : {11, 22}) { + std::vector phases; + fesa::internal::ScopedPardisoBackendTestHook hook{ + [&](const fesa::internal::PardisoBackendTestCall& call, + MKL_INT& error, MKL_INT&) { + phases.push_back(static_cast(call.phase)); + if (call.phase == failing_phase) error = -8; + }}; + { + fesa::MklPardisoSolver solver; + const auto status = solver.Factorize(matrix); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Diagnostics().front().code, "pardiso-integer-overflow"); + fesa::Vector output{1U, 29.0}; + EXPECT_FALSE(solver.Solve(fesa::Vector{1U, 1.0}, output).IsOk()); + EXPECT_DOUBLE_EQ(output[0], 29.0); + } + EXPECT_EQ(phases, failing_phase == 11 ? (std::vector{11, -1}) + : (std::vector{11, 22, -1})); + } + std::vector phases; + fesa::internal::ScopedPardisoBackendTestHook hook{ + [&](const fesa::internal::PardisoBackendTestCall& call, + MKL_INT&, MKL_INT&) { + phases.push_back(static_cast(call.phase)); + if (call.phase == 11) throw std::runtime_error("injected unwind"); + }}; + { + fesa::MklPardisoTangentSolver solver; + EXPECT_THROW(solver.FactorizeFresh(matrix), std::runtime_error); + EXPECT_EQ(phases, (std::vector{11, -1})); + } + EXPECT_EQ(phases, (std::vector{11, -1})); +} + +TEST(PardisoSession, FailedTangentSolveRetainsStateButFailedFreshInvalidatesIt) { + bool fail_solve = true; + fesa::internal::ScopedPardisoBackendTestHook hook{ + [&](const fesa::internal::PardisoBackendTestCall& call, + MKL_INT& error, MKL_INT&) { + if (call.phase == 33 && fail_solve) error = -3; + }}; + fesa::MklPardisoTangentSolver solver; + ASSERT_TRUE(solver.FactorizeFresh( + MakeDenseCsr(2U, 2U, {2.0, 1.0, 0.0, 3.0})).IsOk()); + fesa::Vector correction{2U, 31.0}; + auto status = solver.SolveCorrection(fesa::Vector{2U, 6.0}, correction); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Diagnostics().front().code, "pardiso-solve-failed"); + EXPECT_DOUBLE_EQ(correction[0], 31.0); + EXPECT_DOUBLE_EQ(correction[1], 31.0); + fail_solve = false; + ASSERT_TRUE(solver.SolveCorrection(fesa::Vector{2U, 6.0}, correction).IsOk()); + EXPECT_NEAR(correction[0], 2.0, 1e-14); + EXPECT_NEAR(correction[1], 2.0, 1e-14); + status = solver.FactorizeFresh(MakeDenseCsr(1U, 2U, {1.0, 2.0})); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Diagnostics().front().code, "solver-matrix-not-square"); + status = solver.SolveCorrection(fesa::Vector{2U, 6.0}, correction); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Diagnostics().front().code, "solver-not-factorized"); + EXPECT_NEAR(correction[0], 2.0, 1e-14); + EXPECT_NEAR(correction[1], 2.0, 1e-14); +} + +TEST(PardisoSession, EmptyLifecycleNeverInvokesBackend) { + int calls = 0; + fesa::internal::ScopedPardisoBackendTestHook hook{ + [&](const fesa::internal::PardisoBackendTestCall&, MKL_INT&, MKL_INT&) { + ++calls; + }}; + { + fesa::MklPardisoSolver solver; + fesa::MklPardisoTangentSolver tangent; + const auto empty = MakeDenseCsr(0U, 0U, {}); + fesa::Vector solution{0U}; + ASSERT_TRUE(solver.Factorize(empty).IsOk()); + ASSERT_TRUE(solver.Solve(fesa::Vector{0U}, solution).IsOk()); + ASSERT_TRUE(solver.Factorize(empty).IsOk()); + ASSERT_TRUE(tangent.FactorizeFresh(empty).IsOk()); + ASSERT_TRUE(tangent.SolveCorrection(fesa::Vector{0U}, solution).IsOk()); + ASSERT_TRUE(tangent.FactorizeFresh(empty).IsOk()); + } + EXPECT_EQ(calls, 0); +} + +TEST(PardisoSession, RestoresDefaultThreadPolicyAndPreservesCallerOverride) { + const int previous = mkl_set_num_threads_local(0); + struct RestoreThreads { + int previous; + ~RestoreThreads() { mkl_set_num_threads_local(previous); } + } restore{previous}; + const auto matrix = MakeDenseCsr(1U, 1U, {2.0}); + for (const int caller_threads : {0, 2}) { + mkl_set_num_threads_local(caller_threads); + int release_calls = 0; + fesa::internal::ScopedPardisoBackendTestHook hook{ + [&](const fesa::internal::PardisoBackendTestCall& call, + MKL_INT& error, MKL_INT&) { + EXPECT_EQ(mkl_get_max_threads(), caller_threads == 0 ? 1 : 2); + if (call.phase == 22) error = -8; + if (call.phase == -1) ++release_calls; + }}; + { + fesa::MklPardisoSolver solver; + EXPECT_FALSE(solver.Factorize(matrix).IsOk()); + EXPECT_EQ(mkl_set_num_threads_local(caller_threads), caller_threads); + } + EXPECT_EQ(release_calls, 1); + EXPECT_EQ(mkl_set_num_threads_local(caller_threads), caller_threads); + } +} diff --git a/tests/unit/solvers/linear/mkl_pardiso_solver_test.cpp b/tests/unit/solvers/linear/mkl_pardiso_solver_test.cpp index 4758e70..1a6953e 100644 --- a/tests/unit/solvers/linear/mkl_pardiso_solver_test.cpp +++ b/tests/unit/solvers/linear/mkl_pardiso_solver_test.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -12,7 +11,6 @@ #include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" -#include "math/dense_blas_internal.h" namespace { @@ -125,42 +123,6 @@ TEST(MklPardisoSolver, ReusesOneFactorizationForRepeatedRhs) { EXPECT_LE(NormalizedResidual(matrix, second, rhs_second), 1.0e-10); } -TEST(MklPardisoSolver, SingleEquationPolicyPreservesLifecycleAndConcurrentTls) { - const auto worker = [] { - 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 default_threads = mkl_get_max_threads(); - fesa::MklPardisoSolver solver; - for (int repeat = 0; repeat < 10; ++repeat) { - if (!solver.Factorize(MakeDenseCsr(1U, {2.0})).IsOk()) return false; - auto solution = MakeVector({9.0}); - if (!solver.Solve(MakeVector({4.0}), solution).IsOk() || - solution[0U] != 2.0) return false; - if (!solver.Factorize(MakeDenseCsr(1U, {4.0})).IsOk()) return false; - if (!solver.Solve(MakeVector({4.0}), solution).IsOk() || - solution[0U] != 1.0) return false; - auto invalid_rhs = MakeVector({std::numeric_limits::quiet_NaN()}); - if (solver.Solve(invalid_rhs, solution).IsOk() || solution[0U] != 1.0) - return false; - if (!solver.Solve(MakeVector({8.0}), solution).IsOk() || - solution[0U] != 2.0) return false; - if (solver.Factorize(MakeDenseCsr(1U, {-1.0})).IsOk()) return false; - if (solver.Solve(MakeVector({8.0}), solution).IsOk() || - solution[0U] != 2.0) return false; - if (mkl_get_max_threads() != default_threads || - mkl_set_num_threads_local(0) != 0) return false; - } - return true; - }; - auto first = std::async(std::launch::async, worker); - auto second = std::async(std::launch::async, worker); - EXPECT_TRUE(first.get()); - EXPECT_TRUE(second.get()); -} - TEST(MklPardisoSolver, StillRejectsNonsymmetricPublicCsr) { const auto nonsymmetric = MakeDenseCsr(2U, {2.0, 1.0, 0.0, 3.0}); fesa::MklPardisoSolver solver; @@ -193,32 +155,6 @@ TEST(MklPardisoSolver, RefactorizesWithoutLeakingState) { 1.0e-10); } -TEST(MklPardisoSolver, SingleEquationScopeTransitionsBetweenRetainedSizes) { - 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 default_threads = mkl_get_max_threads(); - fesa::MklPardisoSolver solver; - ASSERT_TRUE(solver.Factorize(MakeDenseCsr(1U, {2.0})).IsOk()); - auto single = MakeVector({0.0}); - ASSERT_TRUE(solver.Solve(MakeVector({4.0}), single).IsOk()); - EXPECT_DOUBLE_EQ(single[0U], 2.0); - ASSERT_TRUE(solver.Factorize(MakeDenseCsr(2U, {2, 0, 0, 3})).IsOk()); - fesa::Vector pair{2U}; - ASSERT_TRUE(solver.Solve(MakeVector({4, 6}), pair).IsOk()); - EXPECT_NEAR(pair[0U], 2, 1e-14); - EXPECT_NEAR(pair[1U], 2, 1e-14); - EXPECT_EQ(mkl_get_max_threads(), default_threads); - EXPECT_EQ(mkl_set_num_threads_local(0), 0); - ASSERT_TRUE(solver.Factorize(MakeDenseCsr(1U, {4.0})).IsOk()); - ASSERT_TRUE(solver.Solve(MakeVector({8.0}), single).IsOk()); - EXPECT_DOUBLE_EQ(single[0U], 2.0); - EXPECT_EQ(mkl_get_max_threads(), default_threads); - EXPECT_EQ(mkl_set_num_threads_local(0), 0); -} - TEST(MklPardisoSolver, ClassifiesSingularIndefiniteAndNonfiniteFailures) { fesa::MklPardisoSolver solver; diff --git a/tests/unit/solvers/linear/tangent_solver_test.cpp b/tests/unit/solvers/linear/tangent_solver_test.cpp index bb7096f..0dab893 100644 --- a/tests/unit/solvers/linear/tangent_solver_test.cpp +++ b/tests/unit/solvers/linear/tangent_solver_test.cpp @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -14,7 +13,6 @@ #include "fesa/math/sparse_matrix.h" #include "fesa/math/vector.h" #include "fesa/solvers/linear/tangent_solver.h" -#include "math/dense_blas_internal.h" namespace { @@ -131,104 +129,4 @@ TEST(TangentSolver, RejectsSolveBeforeSuccessfulFactorization) { EXPECT_DOUBLE_EQ(correction[1U], -9.0); } -TEST(TangentSolver, ReusesRhsButFreshValuesAndFailureCannotReuseStaleState) { - fesa::MklPardisoTangentSolver solver; - ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(2U, {2.0, 1.0, 0.0, 3.0})) - .IsOk()); - fesa::Vector correction{2U}; - for (double scale : {1.0, -2.0, 0.5}) { - ASSERT_TRUE(solver.SolveCorrection(MakeVector({4.0 * scale, 6.0 * scale}), - correction).IsOk()); - EXPECT_NEAR(correction[0U], scale, 1.0e-14); - EXPECT_NEAR(correction[1U], 2.0 * scale, 1.0e-14); - } - const auto bad_rhs = MakeVector({std::numeric_limits::quiet_NaN(), 0}); - correction = MakeVector({17, -4}); - EXPECT_FALSE(solver.SolveCorrection(bad_rhs, correction).IsOk()); - EXPECT_DOUBLE_EQ(correction[0U], 17); - EXPECT_DOUBLE_EQ(correction[1U], -4); - ASSERT_TRUE(solver.SolveCorrection(MakeVector({4, 6}), correction).IsOk()); - ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(2U, {4, 0, 1, 2})).IsOk()); - ASSERT_TRUE(solver.SolveCorrection(MakeVector({4, 5}), correction).IsOk()); - EXPECT_NEAR(correction[0U], 1, 1.0e-14); - EXPECT_NEAR(correction[1U], 2, 1.0e-14); - auto diagonal = fesa::SparseMatrix::FromCoo( - 2U, 2U, {{0U, 0U, 5, 0U, 0U}, {1U, 1U, 7, 1U, 0U}}, - {{0U, 1U, 2U}, {0U, 1U}}); - ASSERT_TRUE(diagonal.HasValue()); - ASSERT_TRUE(solver.FactorizeFresh(diagonal.Value()).IsOk()); - ASSERT_TRUE(solver.SolveCorrection(MakeVector({10, -7}), correction).IsOk()); - EXPECT_NEAR(correction[0U], 2, 1.0e-14); - EXPECT_NEAR(correction[1U], -1, 1.0e-14); - EXPECT_FALSE(solver.FactorizeFresh(MakeDenseCsr(2U, {0, 0, 0, 0})).IsOk()); - correction = MakeVector({19, -8}); - const auto stale = solver.SolveCorrection(MakeVector({10, -7}), correction); - ASSERT_FALSE(stale.IsOk()); - EXPECT_EQ(stale.Diagnostics().front().code, "solver-not-factorized"); - EXPECT_DOUBLE_EQ(correction[0U], 19); - EXPECT_DOUBLE_EQ(correction[1U], -8); - ASSERT_TRUE(solver.FactorizeFresh(diagonal.Value()).IsOk()); - EXPECT_TRUE(solver.SolveCorrection(MakeVector({10, -7}), correction).IsOk()); -} - -TEST(TangentSolver, SingleEquationScopeRestoresTlsAcrossFreshFailureAndReuse) { - const auto worker = [] { - 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 default_threads = mkl_get_max_threads(); - fesa::MklPardisoTangentSolver solver; - for (int repeat = 0; repeat < 10; ++repeat) { - auto correction = MakeVector({17.0}); - if (!solver.FactorizeFresh(MakeDenseCsr(1U, {2.0})).IsOk()) return false; - if (!solver.SolveCorrection(MakeVector({4.0}), correction).IsOk() || - correction[0U] != 2.0) return false; - if (!solver.FactorizeFresh(MakeDenseCsr(1U, {4.0})).IsOk()) return false; - if (!solver.SolveCorrection(MakeVector({4.0}), correction).IsOk() || - correction[0U] != 1.0) return false; - if (solver.SolveCorrection(MakeVector({ - std::numeric_limits::infinity()}), correction).IsOk() || - correction[0U] != 1.0) return false; - if (solver.FactorizeFresh(MakeDenseCsr(1U, {0.0})).IsOk()) return false; - if (solver.SolveCorrection(MakeVector({4.0}), correction).IsOk() || - correction[0U] != 1.0) return false; - if (mkl_get_max_threads() != default_threads || - mkl_set_num_threads_local(0) != 0) return false; - } - return true; - }; - auto first = std::async(std::launch::async, worker); - auto second = std::async(std::launch::async, worker); - EXPECT_TRUE(first.get()); - EXPECT_TRUE(second.get()); -} - -TEST(TangentSolver, SingleEquationScopeTransitionsBetweenRetainedSizes) { - 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 default_threads = mkl_get_max_threads(); - fesa::MklPardisoTangentSolver solver; - ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(1U, {2.0})).IsOk()); - auto single = MakeVector({0.0}); - ASSERT_TRUE(solver.SolveCorrection(MakeVector({4.0}), single).IsOk()); - EXPECT_DOUBLE_EQ(single[0U], 2.0); - ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(2U, {2, 1, 0, 3})).IsOk()); - fesa::Vector pair{2U}; - ASSERT_TRUE(solver.SolveCorrection(MakeVector({6, 6}), pair).IsOk()); - EXPECT_NEAR(pair[0U], 2, 1e-14); - EXPECT_NEAR(pair[1U], 2, 1e-14); - EXPECT_EQ(mkl_get_max_threads(), default_threads); - EXPECT_EQ(mkl_set_num_threads_local(0), 0); - ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(1U, {4.0})).IsOk()); - ASSERT_TRUE(solver.SolveCorrection(MakeVector({8.0}), single).IsOk()); - EXPECT_DOUBLE_EQ(single[0U], 2.0); - EXPECT_EQ(mkl_get_max_threads(), default_threads); - EXPECT_EQ(mkl_set_num_threads_local(0), 0); -} - } // namespace