diff --git a/.clang-format b/.clang-format index a00ad8b..6a25b94 100644 --- a/.clang-format +++ b/.clang-format @@ -100,7 +100,9 @@ CompactNamespaces: false KeepEmptyLinesAtTheStartOfBlocks: false MaxEmptyLinesToKeep: 1 -# Include sorting - ExaConstit headers first, then TPLs, then stdlib +# Include sorting - ExaConstit headers first, then MFEM, then other TPLs, then stdlib +# CaseSensitive is set explicitly since clang-format >= 13 defaults to case-insensitive +# matching, which would otherwise pull headers like "SNLS_config.h" into the wrong group # NOTE: clang-format will alphabetize within each priority group SortIncludes: CaseSensitive IncludeBlocks: Regroup @@ -108,17 +110,26 @@ IncludeCategories: # ExaConstit project headers (quoted includes from project directories) - Regex: '^"(boundary_conditions|fem_operators|mfem_expt|models|options|postprocessing|sim_state|solvers|system_driver|umats|utilities)/' Priority: 1 + CaseSensitive: true - Regex: '^"(system_driver)' Priority: 1 - # Third-party library headers (RAJA, mfem, ecmech, snls, caliper) - - Regex: '^"(RAJA|mfem|ecmech|snls|caliper)' + CaseSensitive: true + # MFEM headers get their own group + - Regex: '^"mfem(\.hpp"|/)' Priority: 2 + CaseSensitive: true + # Other third-party library headers (RAJA, ECMech, SNLS, caliper, umpire) + - Regex: '^["<](RAJA|ECMech|ecmech|SNLS|snls|caliper|umpire)' + Priority: 3 + CaseSensitive: true # Standard C++ library (angle brackets, no file extension) - Regex: '^<[a-z_]+>$' - Priority: 3 + Priority: 4 + CaseSensitive: true # C standard library (angle brackets with .h extension) - Regex: '^<[a-z_]+\.h>$' - Priority: 3 + Priority: 4 + CaseSensitive: true # Catch-all for anything else - Regex: '.*' Priority: 99 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 77413db..1ba1145 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,4 @@ # .git-blame-ignore-revs # First clang-format run so lots of changes... -fec3a715e3c8c9e86a92c03f8e6501ad58bbe68f \ No newline at end of file +fec3a715e3c8c9e86a92c03f8e6501ad58bbe68f +726c934ef7ae9ba95e4b63cccce7799b603bcf8d \ No newline at end of file diff --git a/.github/workflows/build-mfem/action.yml b/.github/workflows/build-mfem/action.yml index 7eaa887..0aa103d 100644 --- a/.github/workflows/build-mfem/action.yml +++ b/.github/workflows/build-mfem/action.yml @@ -17,7 +17,7 @@ inputs: mfem-branch: description: 'Branch to checkout' required: false - default: 'exaconstit-dev' + default: 'exaconstit-latest' mfem-dir: description: 'MFEM top directory name' required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ff47f4..f12c232 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,11 +11,12 @@ on: # Note the SNLS top dir is no longer where SNLS's source is located within ecmech # rather it's the top directory of ecmech. env: - HYPRE_ARCHIVE: v2.30.0.tar.gz - HYPRE_TOP_DIR: hypre-2.30.0 + HYPRE_ARCHIVE: v3.1.0.tar.gz + HYPRE_TOP_DIR: hypre-3.1.0 METIS_ARCHIVE: metis-5.1.0.tar.gz METIS_TOP_DIR: metis-5.1.0 MFEM_TOP_DIR: mfem-exaconstit + MFEM_BRANCH: exaconstit-latest ECMECH_TOP_DIR: ecmech SNLS_TOP_DIR: ecmech RAJA_TOP_DIR: RAJA @@ -143,7 +144,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ env.MFEM_TOP_DIR }} - key: ${{ runner.os }}-build-${{ env.MFEM_TOP_DIR }}-v2.06 + key: ${{ runner.os }}-build-${{ env.MFEM_TOP_DIR }}-${{ env.MFEM_BRANCH }}-${{ env.HYPRE_TOP_DIR }}-v2.07 - name: install mfem if: matrix.mpi == 'parallel' && steps.mfem-cache.outputs.cache-hit != 'true' @@ -153,6 +154,7 @@ jobs: hypre-dir: '${{ github.workspace }}/${{ env.HYPRE_TOP_DIR }}/src/hypre/' metis-dir: '${{ github.workspace }}/${{ env.METIS_TOP_DIR }}/install_dir/' mfem-dir: ${{ env.MFEM_TOP_DIR }} + mfem-branch: ${{ env.MFEM_BRANCH }} # ExaConstit build and test - name: build diff --git a/scripts/install/common/dependency_versions.sh b/scripts/install/common/dependency_versions.sh index 86f22fe..94b5180 100644 --- a/scripts/install/common/dependency_versions.sh +++ b/scripts/install/common/dependency_versions.sh @@ -2,25 +2,25 @@ # Central version control for all dependencies # Portability libraries -export CAMP_VER="v2025.09.2" -export RAJA_VER="v2025.09.1" +export CAMP_VER="v2025.12.0" +export RAJA_VER="v2025.12.2" #export UMPIRE_VER="v2025.09.0" # For now we need something a little pass the v2025.09.0 release # for Umpire as we need a small bug fix for any build with Umpire -export UMPIRE_VER="54a1909e91ce9604328977974e9b1002bf9f8781" -export CHAI_VER="v2025.09.1" +export UMPIRE_VER="v2025.12.0" +export CHAI_VER="v2025.12.0" # Material models export EXACMECH_REPO="https://github.com/LLNL/ExaCMech.git" export EXACMECH_BRANCH="develop" # FEM infrastructure -export HYPRE_VER="v2.32.0" +export HYPRE_VER="v3.1.0" export METIS_VER="5.1.0" export METIS_URL="https://mfem.github.io/tpls/metis-${METIS_VER}.tar.gz" export MFEM_REPO="https://github.com/rcarson3/mfem.git" -export MFEM_BRANCH="exaconstit-dev" +export MFEM_BRANCH="exaconstit-latest" # Main application export EXACONSTIT_REPO="https://github.com/llnl/ExaConstit.git" diff --git a/scripts/postprocessing/xtal_light_up/Cargo.lock b/scripts/postprocessing/xtal_light_up/Cargo.lock index 1e98946..0269384 100644 --- a/scripts/postprocessing/xtal_light_up/Cargo.lock +++ b/scripts/postprocessing/xtal_light_up/Cargo.lock @@ -1,100 +1,68 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "indoc" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "inventory" -version = "0.3.15" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" +checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c" +dependencies = [ + "rustversion", +] [[package]] name = "libc" -version = "0.2.155" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "lock_api" -version = "0.4.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "matrixmultiply" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" dependencies = [ "autocfg", "rawpointer", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "ndarray" -version = "0.15.6" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" dependencies = [ "matrixmultiply", "num-complex", "num-integer", "num-traits", + "portable-atomic", + "portable-atomic-util", "rawpointer", ] @@ -109,9 +77,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -128,9 +96,9 @@ dependencies = [ [[package]] name = "numpy" -version = "0.21.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec170733ca37175f5d75a5bea5911d6ff45d2cd52849ce98b685394e4f2f37f4" +checksum = "6a5b15d63a5ff39e378daed0e1340d3a5964703ea9712eb09a0dc66fade996f4" dependencies = [ "libc", "ndarray", @@ -138,88 +106,70 @@ dependencies = [ "num-integer", "num-traits", "pyo3", + "pyo3-build-config", "rustc-hash", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "parking_lot" -version = "0.12.3" +name = "portable-atomic" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] -name = "parking_lot_core" -version = "0.9.10" +name = "portable-atomic-util" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", + "portable-atomic", ] -[[package]] -name = "portable-atomic" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" - [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.21.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" +checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655" dependencies = [ "anyhow", - "cfg-if", - "indoc", "inventory", "libc", - "memoffset", - "parking_lot", + "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.21.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" +checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.21.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" +checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330" dependencies = [ "libc", "pyo3-build-config", @@ -227,9 +177,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.21.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" +checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -239,22 +189,21 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.21.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" +checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -265,38 +214,23 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -dependencies = [ - "bitflags", -] - [[package]] name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "scopeguard" -version = "1.2.0" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] -name = "smallvec" -version = "1.13.2" +name = "rustversion" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "syn" -version = "2.0.71" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -305,85 +239,15 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.15" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unindent" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "xtal_light_up" diff --git a/scripts/postprocessing/xtal_light_up/Cargo.toml b/scripts/postprocessing/xtal_light_up/Cargo.toml index b9313f3..02d790a 100644 --- a/scripts/postprocessing/xtal_light_up/Cargo.toml +++ b/scripts/postprocessing/xtal_light_up/Cargo.toml @@ -2,14 +2,16 @@ name = "xtal_light_up" version = "0.1.0" edition = "2021" +# as_chunks needs Rust 1.88+ +rust-version = "1.88" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = {version="1.0"} +anyhow = {version="1.0.104"} # rayon = {version="1.10"} -numpy = {version = "0.21", optional=false} -pyo3 = { version = "0.21", optional=false, features = ["abi3-py39", "extension-module","anyhow", "multiple-pymethods",] } +numpy = {version = "0.29", optional=false} +pyo3 = { version = "0.29", optional=false, features = ["abi3-py39", "extension-module","anyhow", "multiple-pymethods",] } num-traits = {version = "0.2.14", features = ["libm"]} [features] diff --git a/scripts/postprocessing/xtal_light_up/README.md b/scripts/postprocessing/xtal_light_up/README.md new file mode 100644 index 0000000..16af629 --- /dev/null +++ b/scripts/postprocessing/xtal_light_up/README.md @@ -0,0 +1,170 @@ +# xtal_light_up + +Fast lattice strain / "light-up" post-processing for ExaConstit results. This +directory contains a Rust library with Python bindings, built with +[PyO3](https://pyo3.rs). It computes the quantities a powder diffraction +("light-up") experiment measures, from ExaConstit's visualization output: +- lattice strains; +- directional stiffness; +- Taylor factors; +- equivalent plastic strain rates; +- in-fiber volumes. + +The calculations mirror ExaConstit's in-situ light-up +(`LightUp` in `src/postprocessing/mechanics_lightup.cpp`), so both give the +same results for every crystal system ExaConstit supports. This module is useful +when you didn't turn on the in-situ light-up for a run, or want to try other +HKLs, sample directions or tolerances afterwards. + +## Contents + +| Path | What it is | +|------|------------| +| `src/lib.rs` | Crate root | +| `src/pyxtal_light_up/mod.rs` | The Python-facing functions (see [API](#api)) | +| `src/pyxtal_light_up/lattice.rs` | Crystal systems: symmetry operators, lattice parameters, reciprocal lattice | +| `src/pyxtal_light_up/math.rs` | Small fixed-size linear algebra helpers | +| `light_up_py/fiber_calcs_rank.py` | Script that processes an ExaConstit ADIOS2 output end to end | +| `pyxtallu.py` | Small example that runs each function and compares the in-fiber check with HEXRD | +| `setup.py`, `xtal_light_up/` | Python packaging (setuptools-rust) | + +## Building and installing + +Requirements: +- **Rust 1.88 or newer:** for example from . +- **Python 3.9 or newer, with NumPy.** +- **Build tools:** `pip install setuptools-rust setuptools wheel`. + +From this directory: + +```bash +pip install . # regular install +# or +python setup.py develop --user # editable install into user site-packages +``` + +Then: + +```python +import xtal_light_up.xtal_light_up as xlup +help(xlup) # module and per-function documentation +``` + +To run `light_up_py/fiber_calcs_rank.py` you'll also need the ADIOS2 Python +bindings (`import adios2`, with `adios2.FileReader` available). `pyxtallu.py` +also needs [HEXRD](https://github.com/HEXRD/hexrd). + +## What's needed from ExaConstit + +The functions work on per-element data from an ExaConstit run that uses an +ExaCMech crystal plasticity model. `fiber_calcs_rank.py` reads that data from the +ADIOS2 visualization output, which requires: + +1. **ADIOS2 output:** set `adios2 = true` under `[Visualizations]` in the options + file. MFEM must be built with ADIOS2 (`MFEM_USE_ADIOS2`). The `.bp` output is + written to the visualization output location + (`[output_directory]/[basename]/[floc]`, see `Visualizations.floc` in + `src/options.toml`). +2. **The fields below projected:** `[PostProcessing.projections]` does this by + default with `auto_enable_compatible = true`. Otherwise, list them in + `enabled_projections`. + + | `enabled_projections` | ADIOS2 variable | Shape per element | Used for | + |-----------------------|-----------------|-------------------|----------| + | `"volume"` | `Element Volumes` | scalar | volume weighting | + | `"xtal_ori"` | `Crystal Orientations` | quaternion `[w, x, y, z]` | which elements are in each fiber | + | `"elastic_strain"` | `Elastic Strains` | 6 (sample frame) | lattice strains, directional stiffness | + | `"cauchy"` | `Cauchy Stress` | 6 | directional stiffness | + | `"shear_rate"` | `Shearing Rate` | number of slip systems | Taylor factor | + | `"dpeff"` | `Equivalent Plastic Strain Rate` | scalar | Taylor factor, equivalent plastic strain rate | + +3. **The number of MPI ranks the run used** (`-nranks` for the script). The + ADIOS2 output is written as one block per rank, and every block is read. + +The equivalent plastic strain rate comes straight from ExaCMech. It isn't +recomputed from the slip rates, so the Taylor factor works for any slip system +set. The elastic strains are expected in the sample frame, which is how +ExaConstit v0.9+ writes them. `strain_lattice2sample` converts lattice frame +strains from older runs. + +## Crystal systems + +`calc_within_fibers` takes the same crystal system names and lattice parameters +as ExaConstit's in-situ light-up options (`laue_type` and `lattice_params`). Any +angles are in radians. + +| `lattice_type` | Symmetry operations | `lattice_params` | +|----------------|---------------------|------------------| +| `cubic` (default) | 24 | a | +| `hexagonal` | 12 | a, c | +| `trigonal` | 6 | a, c | +| `rhombohedral` | 6 | a, alpha | +| `tetragonal` | 8 | a, c | +| `orthorhombic` | 4 | a, b, c | +| `monoclinic` | 2 | a, b, c, beta | +| `triclinic` | 1 | a, b, c, alpha, beta, gamma | + +HKLs are given as 3-index Miller indices, also for hexagonal and trigonal +crystals. For example, (10-10) is `[1, 0, 0]`. + +## API + +All arrays must be C-contiguous NumPy arrays: `float64`, or `bool` for the +in-fiber flags. Arrays are laid out as `(steps, elements, ...)` per step and +element. Symmetric tensors are 6-vectors `[11, 22, 33, 23, 13, 12]` of tensor +(not engineering shear) components. Output arguments are filled in place; the +"added to" outputs accumulate across calls. Call `help()` on a function for the +full details. + +| Function | Computes | +|----------|----------| +| `calc_within_fibers(lattice_orientations, sample_dir, hkls, lattice_params, distance_tolerance_rad, in_fibers, lattice_type="cubic")` | `(nhkl, steps, elems)` flags for which elements are in each HKL fiber | +| `calc_lattice_strains(sample_strains, sample_dir, elem_vols, in_fibers, lattice_strains, lattice_volumes, per_rank_update)` | Volume weighted lattice strain and in-fiber volume per HKL and step | +| `calc_directional_stiffness_lattice_fiber(cauchy_stress, sample_strain, directional_stiffness, elem_vols, in_fibers, per_rank_update)` | Volume weighted 33 stress / 33 strain per HKL and step | +| `calc_taylor_factors_lattice_fiber(shear_rates, eq_pl_strain_rate, taylor_factors, lattice_eq_pl_strain_rate, elem_vols, in_fibers, per_rank_update)` | Volume weighted Taylor factor and equivalent plastic strain rate per HKL and step | +| `calc_directional_stiffness(cauchy_stress, sample_strain, directional_stiffness)` | Per-element 33 stress / 33 strain | +| `calc_taylor_factors(shear_rates, eq_pl_strain_rate, taylor_factors)` | Per-element Taylor factor | +| `strain_lattice2sample(lattice_orientations, strains)` | Rotates lattice frame strains to the sample frame in place | + +**`per_rank_update`:** +- **`True`:** the fiber functions add raw volume weighted sums to their outputs. + This lets you accumulate the results from every rank's block, then divide by + `lattice_volumes` once at the end. `fiber_calcs_rank.py` does this. +- **`False`:** each call adds the volume average of just the elements passed in. + +### Quick example + +```python +import numpy as np +import xtal_light_up.xtal_light_up as xlup + +# quats: (steps, elems, 4), strains: (steps, elems, 6), vols: (steps, elems) +hkls = np.array([[1., 0., 0.], [0., 0., 2.], [1., 0., 1.]]) # Ti (10-10), (0002), (10-11) +s_dir = np.array([0.0, 0.0, 1.0]) +in_fibers = np.zeros((hkls.shape[0], steps, elems), dtype=bool) +xlup.calc_within_fibers(quats, s_dir, hkls, [2.95, 4.68], np.deg2rad(5.0), in_fibers, + lattice_type="hexagonal") + +lattice_strains = np.zeros((hkls.shape[0], steps)) +lattice_vols = np.zeros_like(lattice_strains) +xlup.calc_lattice_strains(strains, s_dir, vols, in_fibers, lattice_strains, lattice_vols, True) +lattice_strains /= lattice_vols # volume average per HKL and step +``` + +## Development + +The Rust code should stay free of `cargo clippy` warnings and be formatted with +`cargo fmt`. On macOS, building or testing outside of setuptools-rust needs the +Python symbols left for runtime lookup: + +```bash +export PYO3_PYTHON=$(which python3) +export RUSTFLAGS="-C link-arg=-undefined -C link-arg=dynamic_lookup" # macOS only +cargo fmt --check +cargo clippy --all-targets -- -D warnings +cargo test --lib # symmetry group closure, reciprocal lattice, parameter checks +``` + +`src/pyxtal_light_up/lattice.rs` has to stay in sync with +`GetSymmetryGroups` and `LatticeTypeGeneral::ComputeLatticeBParam` in ExaConstit's +`src/postprocessing/mechanics_lightup.cpp`. If one changes, change the other. diff --git a/scripts/postprocessing/xtal_light_up/light_up_py/fiber_calcs_rank.py b/scripts/postprocessing/xtal_light_up/light_up_py/fiber_calcs_rank.py index ec820d6..ecd1919 100644 --- a/scripts/postprocessing/xtal_light_up/light_up_py/fiber_calcs_rank.py +++ b/scripts/postprocessing/xtal_light_up/light_up_py/fiber_calcs_rank.py @@ -1,3 +1,60 @@ +""" +Computes lattice strain / "light-up" quantities from an ExaConstit ADIOS2 +visualization output (.bp) using the xtal_light_up Rust module. + +For each HKL fiber (elements whose crystal orientation puts one of the HKL's +symmetrically equivalent plane normals within a tolerance of the sample +direction) and every output step it computes the volume averaged: + +- lattice strain (sample frame elastic strain projected on the sample direction) +- directional stiffness (sample frame 33 Cauchy stress / 33 elastic strain) +- Taylor factor (sum of |slip system shear rates| / equivalent plastic strain rate) +- equivalent plastic strain rate +- in-fiber volume + +These are the same quantities ExaConstit's in-situ light-up computes, just done +afterwards from the visualization output, for every crystal system ExaConstit +supports. + +Requirements on the ExaConstit run (ExaCMech material model): + +- [Visualizations] adios2 = true, which needs MFEM built with ADIOS2 + (MFEM_USE_ADIOS2). The .bp output is written to the visualization output + location ([output_directory]/[basename]/[floc], see Visualizations.floc in + options.toml); runs with output split by region write one .bp per region. +- [PostProcessing.projections] must include these fields, which is the default + with auto_enable_compatible = true, or otherwise list them in + enabled_projections: + + =================== =============================== ========================== + enabled_projections ADIOS2 variable read used for + =================== =============================== ========================== + "volume" Element Volumes volume weighting + "xtal_ori" Crystal Orientations which elements are in fiber + "elastic_strain" Elastic Strains lattice strain, stiffness + "cauchy" Cauchy Stress directional stiffness + "shear_rate" Shearing Rate Taylor factor + "dpeff" Equivalent Plastic Strain Rate Taylor factor, eq. pl. rate + =================== =============================== ========================== + +- The number of MPI ranks the simulation ran on (-nranks), since the ADIOS2 + output is written as one block per rank and every block has to be read. + +Elastic strains are expected in the sample frame, which ExaConstit v0.9+ writes. +The directional stiffness, Taylor factor and equivalent plastic strain rate have +one fewer entry than the number of output steps, since the initial (undeformed) +output has no stress / rate data to go with it. + +Usage: + python fiber_calcs_rank.py -in_dir \ + -out_dir ./outdir -out_basename rve_ -nranks 48 \ + -lattice_type hexagonal -lattice_params 2.95 4.68 -distance_tolerance_deg 5 + +The HKLs and sample direction are set near the top of fiber_calc_ranks. Results +are printed and saved to /lattice_avg_quants.npz with the +arrays lattice_strains, lattice_vols (nhkl, steps), lattice_dir_stiff, +lattice_tay_fact, lattice_eps_rate (nhkl, steps - 1), hkls and total_volume. +""" # Python standard library imports import argparse import os @@ -11,6 +68,18 @@ import xtal_light_up.xtal_light_up as xlup def fiber_calc_ranks(args): + """ + Reads the ExaConstit ADIOS2 output one rank's block at a time, accumulates the + volume weighted light-up quantities for every HKL fiber, then normalizes them + by the in-fiber volumes and prints / saves the results. + + Parameters + ---------- + args : dict + The command line options (see the module docstring / -h): in_dir, + out_dir, out_basename, nranks, lattice_type, lattice_params and + distance_tolerance_deg. + """ ts_total = time.time() # path to ExaConstit ADIOS2 binary-pack (.bp) in_dir = args["in_dir"] @@ -110,6 +179,8 @@ def fiber_calc_ranks(args): # initialize + # HKL planes to compute the fibers for (Miller indices, 3-index form for + # hexagonal / trigonal too) and the sample direction they're measured along hkl = np.zeros((4, 3)) hkl[0, :] = [1,1,1] hkl[1, :] = [2,0,0] @@ -118,6 +189,16 @@ def fiber_calc_ranks(args): s_dir = np.asarray([0.0,0.0,1.0]) + # Crystal system and lattice parameters, using the same names / parameters as + # ExaConstit's in-situ light-up options (laue_type and lattice_params) with any + # angles in radians: + # cubic -> a; hexagonal / trigonal / tetragonal -> a, c; rhombohedral -> a, alpha; + # orthorhombic -> a, b, c; monoclinic -> a, b, c, beta; + # triclinic -> a, b, c, alpha, beta, gamma + lattice_type = args["lattice_type"] + lattice_params = args["lattice_params"] + distance_tolerance = np.deg2rad(args["distance_tolerance_deg"]) + top = fh.read('Element Volumes' , block_id = 0) # If we want per element quantities then uncomment below block @@ -157,25 +238,31 @@ def fiber_calc_ranks(args): top = fh.read('Shearing Rate' , block_id = 0) gdots_local = np.ascontiguousarray(fh.read('Shearing Rate', start = [0, 0], count = [isize, top.shape[1]], step_selection = [0 , steps - 1] , block_id = ii).reshape((steps - 1, isize, top.shape[1]))[:, con1d[ii], :]) + # ExaCMech's equivalent plastic strain rate, used for the Taylor factor the same way the in-situ light-up does + dpeff_local = np.ascontiguousarray(fh.read('Equivalent Plastic Strain Rate', start = [0], count = [isize], step_selection = [0 , steps - 1] , block_id = ii).reshape((steps - 1, isize))[:, con1d[ii]]) + in_fibers_local = np.zeros((hkl.shape[0], steps, elas_strain_local.shape[1]), dtype=bool) - xlup.calc_within_fibers(xtal_oris_local, s_dir, hkl, 3.60, np.deg2rad(5.0), in_fibers_local) + xlup.calc_within_fibers(xtal_oris_local, s_dir, hkl, lattice_params, distance_tolerance, in_fibers_local, lattice_type=lattice_type) in_fiber_local1 = np.ascontiguousarray(in_fibers_local[:,1:steps,:]) ev_local1 = np.ascontiguousarray(ev_local[1:steps,:]) - # All of our local calculations + # All of our local calculations. The per_rank_update = True argument makes these + # add volume weighted sums so every rank's block can be accumulated; they're + # divided by the in-fiber volumes once all blocks have been processed below. # We're already in the sample frame as ExaConstit as of v0.9 automatically converts it for us # xlup.strain_lattice2sample(xtal_oris_local, elas_strain_local) xlup.calc_lattice_strains(elas_strain_local, s_dir, ev_local, in_fibers_local, lattice_strains, lattice_vols, True) xlup.calc_directional_stiffness_lattice_fiber(stress_local, elas_strain_local[1:steps,:,:], lattice_dir_stiff, ev_local1, in_fiber_local1, True) - xlup.calc_taylor_factors_lattice_fiber(gdots_local, lattice_tay_fact, lattice_eps_rate, ev_local1, in_fiber_local1, True) + xlup.calc_taylor_factors_lattice_fiber(gdots_local, dpeff_local, lattice_tay_fact, lattice_eps_rate, ev_local1, in_fiber_local1, True) # If we want per element quantities then uncomment below block # direct_stiffness_local = np.zeros_like(ev_local[1:steps, :]) # tay_fact_local = np.zeros_like(ev_local[1:steps, :]) # eps_rate_local = np.zeros_like(ev_local[1:steps, :]) # xlup.calc_directional_stiffness(stress_local, elas_strain_local[1:steps,:,:], direct_stiffness_local) - # xlup.calc_taylor_factors(gdots_local, tay_fact_local, eps_rate_local) + # xlup.calc_taylor_factors(gdots_local, dpeff_local, tay_fact_local) + # eps_rate_local = dpeff_local # elem_vols[:, index[ii,0]:index[ii,1]] = ev_local # strains[:, index[ii,0]:index[ii,1], :] = elas_strain_local @@ -197,7 +284,7 @@ def fiber_calc_ranks(args): print("Lattice taylor factor:") lattice_tay_fact = lattice_tay_fact / lattice_vols[:,1:steps] print(lattice_tay_fact.T) - print("Lattice plastic deformation rate:") + print("Lattice equivalent plastic strain rate:") lattice_eps_rate = lattice_eps_rate / lattice_vols[:,1:steps] print(lattice_eps_rate.T) print("Lattice directional stiffness:") @@ -250,6 +337,19 @@ def fiber_calc_ranks(args): help = 'number of resource sets used for the simulation (IMPORTANT)' , type = int, default=int(1)) + parser.add_argument('-lattice_type' , + help = 'crystal system (same names as ExaConstit laue_type): cubic, hexagonal, trigonal, rhombohedral, tetragonal, orthorhombic, monoclinic, triclinic' , + type = str, + default='cubic') + parser.add_argument('-lattice_params' , + help = 'lattice parameters for the crystal system with any angles in radians (cubic: a; hexagonal/trigonal/tetragonal: a c; rhombohedral: a alpha; orthorhombic: a b c; monoclinic: a b c beta; triclinic: a b c alpha beta gamma)' , + type = float, + nargs = '+', + default=[3.60]) + parser.add_argument('-distance_tolerance_deg' , + help = 'angular tolerance in degrees for an orientation to be considered in a fiber' , + type = float, + default=5.0) args = parser.parse_args() print(args) diff --git a/scripts/postprocessing/xtal_light_up/pyxtallu.py b/scripts/postprocessing/xtal_light_up/pyxtallu.py index cebc38d..bb314d3 100644 --- a/scripts/postprocessing/xtal_light_up/pyxtallu.py +++ b/scripts/postprocessing/xtal_light_up/pyxtallu.py @@ -1,3 +1,13 @@ +""" +Small example / smoke test for the xtal_light_up Rust module. + +Runs each of the module's functions on a single element and, for the in-fiber +check, also computes the distance to each fiber with HEXRD so the two can be +compared. Requires the xtal_light_up module to be built +(``pip install .`` from this directory) and HEXRD to be installed. + +For processing actual ExaConstit results see light_up_py/fiber_calcs_rank.py. +""" import numpy as np import xtal_light_up.xtal_light_up as xlup @@ -11,7 +21,9 @@ def make_matl(mat_name , sgnum , lparms , hkl_ssq_max = 50 , dmin_angstroms = 0.5) : """ - + Build a HEXRD material, used here for its plane data (reciprocal lattice B and + crystal symmetry) when comparing against HEXRD. + Parameters ---------- mat_name : str @@ -23,8 +35,11 @@ def make_matl(mat_name , sgnum , lparms , hkl_ssq_max = 50 , dmin_angstroms = 0. hkl_ssq_max : int, optional maximum hkl sum of squares (peak upper bound). The default is 50. dmin_angstroms : float, optional - minimum d-spacing in angstroms (alt peak upper bound). The default is 0.6. - + minimum d-spacing in angstroms (alt peak upper bound). The default is 0.5. + + Returns + ------- + hexrd.material.Material """ matl = material.Material(mat_name) @@ -40,10 +55,26 @@ def make_matl(mat_name , sgnum , lparms , hkl_ssq_max = 50 , dmin_angstroms = 0. def applySym(vec, qsym, csFlag=False, cullPM=False, tol=rot.cnst.sqrt_epsf): """ - Apply symmetry group to a single 3-vector (columnar) argument. + Apply a symmetry group to a single 3-vector (columnar) argument, adapted from + HEXRD's version. + + Parameters + ---------- + vec : (3, 1) array + Vector to apply the symmetry operations to. + qsym : (4, nsym) array + Symmetry operations as quaternions (e.g. HEXRD's PlaneData.getQSym()). + csFlag : bool, optional + Centrosymmetry flag: also include -1 * each symmetric vector. + cullPM : bool, optional + Treat +v and -v as duplicates when removing duplicate vectors. + tol : float, optional + Tolerance for considering two vectors duplicates. - csFlag : centrosymmetry flag - cullPM : cull +/- flag + Returns + ------- + (3, m) array + The m unique symmetrically equivalent vectors. """ nsym = qsym.shape[1] Rsym = rot.rotMatOfQuat(qsym) @@ -61,31 +92,37 @@ def applySym(vec, qsym, csFlag=False, cullPM=False, tol=rot.cnst.sqrt_epsf): def distanceToFiber(c, s, q, qsym, **kwargs): """ - Calculate symmetrically reduced distance to orientation fiber. + Calculate the symmetrically reduced distance (angle) of each orientation to + the fiber defined by crystal direction c and sample direction s, adapted from + HEXRD's version. This is the pure Python / HEXRD equivalent of what + xlup.calc_within_fibers computes before applying its tolerance, so the two + can be compared. Parameters ---------- - c : TYPE - DESCRIPTION. - s : TYPE - DESCRIPTION. - q : TYPE - DESCRIPTION. - qsym : TYPE - DESCRIPTION. - **kwargs : TYPE - DESCRIPTION. + c : 3-vector + Crystal direction (HKL components, converted with ``bmatrix`` if given). + s : 3-vector + Sample direction. + q : (4, nq) array + Orientations as quaternions. + qsym : (4, nsym) array + Crystal symmetry operations as quaternions. + **kwargs + ``centrosymmetry`` (bool, default False): also consider -c. + ``bmatrix`` ((3, 3) array, default identity): matrix taking c to the + crystal frame (e.g. the reciprocal lattice B for HKL plane normals). Raises ------ RuntimeError - DESCRIPTION. + If c or s aren't 3-vectors or an unknown keyword argument is given. Returns ------- - d : TYPE - DESCRIPTION. - + d : (nq,) array + Angle in radians between s and the closest symmetrically equivalent + rotated crystal direction for each orientation. """ csymFlag = False B = np.eye(3) @@ -138,6 +175,10 @@ def distanceToFiber(c, s, q, qsym, **kwargs): # help(xlup) +# A single element at a single step: arrays are (steps, elements, ...) just like the +# data read from ExaConstit's output in light_up_py/fiber_calcs_rank.py. +# Orientation quaternion [w, x, y, z] (normalized) and a lattice frame strain +# 6-vector [11, 22, 33, 23, 13, 12]. lattice_orientations = np.zeros((1,1,4)) strains = np.zeros((1,1,6)) @@ -152,64 +193,26 @@ def distanceToFiber(c, s, q, qsym, **kwargs): strains[:,:,4] = 2.0 strains[:,:,5] = 3.0 +# Rotate the strain into the sample frame (in place). ExaConstit v0.9+ already +# writes its "Elastic Strains" in the sample frame, so this is only for older data. xlup.strain_lattice2sample(lattice_orientations, strains) +# Taylor factor = sum of |slip system shear rates| / equivalent plastic strain rate. +# Both come straight from ExaConstit's outputs ("Shearing Rate" and ExaCMech's +# "Equivalent Plastic Strain Rate"), so nothing needs to be recomputed here. The values +# below just stand in for a single element's worth of that output; any number of slip +# systems works. gdots = np.zeros((1,1,12)) gdots[:,:,0] = 1e-4 gdots[:,:,1] = 1e-4 gdots[:,:,2] = -1e-4 +eq_pl_strain_rate = np.full((1,1), 1.0e-4) tay_factor = np.zeros((1,1)) -n_arr = (1 / np.sqrt(3)) * np.array([ - [ 1 , 1 , 1] , - [ 1 , 1 , 1] , - [ 1 , 1 , 1] , - [-1 , 1 , 1] , - [-1 , 1 , 1] , - [-1 , 1 , 1] , - [-1 , -1 , 1] , - [-1 , -1 , 1] , - [-1 , -1 , 1] , - [ 1 , -1 , 1] , - [ 1 , -1 , 1] , - [ 1 , -1 , 1] - ]) - -s_arr = (1 / np.sqrt(2)) * np.array([ - [ 0 , 1 , -1] , - [-1 , 0 , 1] , - [ 1 , -1 , 0] , - [-1 , 0 , -1] , - [ 0 , -1 , 1] , - [ 1 , 1 , 0] , - [ 0 , -1 , -1] , - [ 1 , 0 , 1] , - [-1 , 1 , 0] , - [ 1 , 0 , -1] , - [ 0 , 1 , 1] , - [-1 , -1 , 0] - ]) - -# calculate FCC Schmid tensor -m_arr = np.zeros((n_arr.shape[0] , s_arr.shape[1], n_arr.shape[1])) -for ii in range(m_arr.shape[0]) : - m_arr[ii, :, :] = np.tensordot(s_arr[ii] , n_arr[ii] , axes = 0) - m_arr[ii, :, :] = 0.5 * (m_arr[ii, :, :] + m_arr[ii, :, :].T) -m_arr = np.reshape(m_arr, (n_arr.shape[0] , s_arr.shape[1] * n_arr.shape[1])) -# calculate DpEff from shear strain rates on each slip system -def dp_eff(gdots) : - - Dp = np.sum(m_arr * gdots[: , None] , axis = 0).reshape((3,3)) - - # Dp = Lp + Lp.T - np.diag(Lp.diagonal()) - - return np.sqrt((2 / 3) * np.tensordot(Dp , Dp)) - -dp_effs = np.zeros_like(tay_factor) - -xlup.calc_taylor_factors(gdots, tay_factor, dp_effs) +xlup.calc_taylor_factors(gdots, eq_pl_strain_rate, tay_factor) -# get material symmetry - here is a simple example for IN625 +# In-fiber check: compare HEXRD's distance to each HKL fiber with the Rust version. +# Get material symmetry - here is a simple example for IN625 (FCC, a = 3.60 angstrom) matl = make_matl(mat_name = 'FCC' , sgnum = 225 , lparms = [3.60 ,]) pd = matl.planeData @@ -225,9 +228,11 @@ def dp_eff(gdots) : c_dir = np.atleast_2d(np.dot(pd.latVecOps['B'] , hkl[jj, :].T)).T distance = distanceToFiber(c_dir, s_dir, quats, pd.getQSym()) +# (nhkl, steps, elements) flags; lattice_type / lattice_params use the same names +# and parameters as ExaConstit's laue_type / lattice_params light-up options in_fibers = np.zeros((hkl.shape[0], 1, 1), dtype=bool) -xlup.calc_within_fibers(lattice_orientations, s_dir, hkl, 3.60, np.deg2rad(5.0), in_fibers) +xlup.calc_within_fibers(lattice_orientations, s_dir, hkl, [3.60], np.deg2rad(5.0), in_fibers, lattice_type="cubic") # # compute distance from quaternions to crystallographic fiber # distance = np.degrees(rot.distanceToFiber(c_dir , s_dir , quats , pd.getQSym())) diff --git a/scripts/postprocessing/xtal_light_up/setup.py b/scripts/postprocessing/xtal_light_up/setup.py index 46aa8be..40eb782 100644 --- a/scripts/postprocessing/xtal_light_up/setup.py +++ b/scripts/postprocessing/xtal_light_up/setup.py @@ -1,3 +1,16 @@ +""" +Builds the xtal_light_up Python package, whose xtal_light_up.xtal_light_up +extension module is compiled from the Rust code in src/ using setuptools-rust. + +Requirements: a Rust toolchain (1.88 or newer, e.g. from https://rustup.rs), +Python 3.9 or newer, and ``pip install setuptools-rust setuptools wheel``. + +Install from this directory with either of: + pip install . + python setup.py develop --user # editable, user site-packages + +See README.md for how to use the module with ExaConstit results. +""" from setuptools import setup from setuptools_rust import Binding, RustExtension diff --git a/scripts/postprocessing/xtal_light_up/src/lib.rs b/scripts/postprocessing/xtal_light_up/src/lib.rs index 279d9fb..18db0f1 100644 --- a/scripts/postprocessing/xtal_light_up/src/lib.rs +++ b/scripts/postprocessing/xtal_light_up/src/lib.rs @@ -1,7 +1,12 @@ +//! xtal_light_up: fast lattice strain / "light-up" post-processing for ExaConstit. +//! +//! Built as a Python extension module (see setup.py); the Python facing functions +//! live in [`pyxtal_light_up`]. + extern crate anyhow; +extern crate num_traits as libnum; extern crate numpy; extern crate pyo3; -extern crate num_traits as libnum; // pub mod xtal_light_up; -pub mod pyxtal_light_up; \ No newline at end of file +pub mod pyxtal_light_up; diff --git a/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/lattice.rs b/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/lattice.rs new file mode 100644 index 0000000..02e680f --- /dev/null +++ b/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/lattice.rs @@ -0,0 +1,399 @@ +//! Crystal lattice description for the light-up calculations. +//! +//! This mirrors `LatticeTypeGeneral` / `GetSymmetryGroups` in ExaConstit's +//! in-situ light-up implementation (src/postprocessing/mechanics_lightup.cpp) +//! so the post-processing results line up with what ExaConstit computes in-situ +//! for every supported crystal system: +//! +//! | lattice type | symmetry ops | lattice parameters | +//! |--------------|--------------|---------------------------------------------| +//! | cubic | 24 | a | +//! | hexagonal | 12 | a, c | +//! | trigonal | 6 | a, c | +//! | rhombohedral | 6 | a, alpha (radians) | +//! | tetragonal | 8 | a, c | +//! | orthorhombic | 4 | a, b, c | +//! | monoclinic | 2 | a, b, c, beta (radians) | +//! | triclinic | 1 | a, b, c, alpha, beta, gamma (radians) | + +use anyhow::{bail, Result}; +use std::f64::consts::{FRAC_PI_2, FRAC_PI_3, PI}; + +use super::math::{cross_prod, dot_prod, norm}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LatticeType { + Cubic, + Hexagonal, + Trigonal, + Rhombohedral, + Tetragonal, + Orthorhombic, + Monoclinic, + Triclinic, +} + +impl LatticeType { + #[cfg(test)] + pub const ALL: [LatticeType; 8] = [ + LatticeType::Cubic, + LatticeType::Hexagonal, + LatticeType::Trigonal, + LatticeType::Rhombohedral, + LatticeType::Tetragonal, + LatticeType::Orthorhombic, + LatticeType::Monoclinic, + LatticeType::Triclinic, + ]; + + /// Parses the same names ExaConstit's `laue_type` option uses (case insensitive) + pub fn from_name(name: &str) -> Result { + Ok(match name.trim().to_ascii_uppercase().as_str() { + "CUBIC" => LatticeType::Cubic, + "HEXAGONAL" => LatticeType::Hexagonal, + "TRIGONAL" => LatticeType::Trigonal, + "RHOMBOHEDRAL" => LatticeType::Rhombohedral, + "TETRAGONAL" => LatticeType::Tetragonal, + "ORTHORHOMBIC" => LatticeType::Orthorhombic, + "MONOCLINIC" => LatticeType::Monoclinic, + "TRICLINIC" => LatticeType::Triclinic, + _ => bail!( + "Unknown lattice type '{}'. Valid options are: cubic, hexagonal, trigonal, \ + rhombohedral, tetragonal, orthorhombic, monoclinic, triclinic", + name + ), + }) + } + + /// Lattice parameters expected for this crystal system, in order + pub fn parameter_names(&self) -> &'static str { + match self { + LatticeType::Cubic => "a", + LatticeType::Hexagonal | LatticeType::Trigonal | LatticeType::Tetragonal => "a, c", + LatticeType::Rhombohedral => "a, alpha (in radians)", + LatticeType::Orthorhombic => "a, b, c", + LatticeType::Monoclinic => "a, b, c, beta (in radians)", + LatticeType::Triclinic => "a, b, c, alpha, beta, gamma (in radians)", + } + } + + pub fn num_parameters(&self) -> usize { + match self { + LatticeType::Cubic => 1, + LatticeType::Hexagonal + | LatticeType::Trigonal + | LatticeType::Tetragonal + | LatticeType::Rhombohedral => 2, + LatticeType::Orthorhombic => 3, + LatticeType::Monoclinic => 4, + LatticeType::Triclinic => 6, + } + } + + /// Full set of cell parameters [a, b, c, alpha, beta, gamma] for this system + /// built from the user supplied lattice parameters. + pub fn cell_parameters(&self, lparams: &[f64]) -> Result<[f64; 6]> { + if lparams.len() != self.num_parameters() { + bail!( + "Lattice type {:?} needs {} lattice parameter(s) ({}) but {} were given", + self, + self.num_parameters(), + self.parameter_names(), + lparams.len() + ); + } + if lparams.iter().any(|lp| !lp.is_finite() || *lp <= 0.0) { + bail!("Lattice parameters must all be positive, got {:?}", lparams); + } + let p = lparams; + Ok(match self { + LatticeType::Cubic => [p[0], p[0], p[0], FRAC_PI_2, FRAC_PI_2, FRAC_PI_2], + LatticeType::Hexagonal | LatticeType::Trigonal => { + [p[0], p[0], p[1], FRAC_PI_2, FRAC_PI_2, 2.0 * FRAC_PI_3] + } + LatticeType::Rhombohedral => [p[0], p[0], p[0], p[1], p[1], p[1]], + LatticeType::Tetragonal => [p[0], p[0], p[1], FRAC_PI_2, FRAC_PI_2, FRAC_PI_2], + LatticeType::Orthorhombic => [p[0], p[1], p[2], FRAC_PI_2, FRAC_PI_2, FRAC_PI_2], + LatticeType::Monoclinic => [p[0], p[1], p[2], FRAC_PI_2, p[3], FRAC_PI_2], + LatticeType::Triclinic => [p[0], p[1], p[2], p[3], p[4], p[5]], + }) + } + + /// Point group rotations as [angle (radians), axis_x, axis_y, axis_z]. + /// Unless noted, these are taken from HEXRD: + /// https://github.com/HEXRD/hexrd/blob/3060f506148ee29ef561c48c3331238e41fb928e/hexrd/rotations.py#L1327-L1514 + pub fn symmetry_angle_axis(&self) -> Vec<[f64; 4]> { + let sqrt3_2 = f64::sqrt(3.0) / 2.0; + let isqrt6 = 1.0 / f64::sqrt(6.0); + match self { + LatticeType::Cubic => vec![ + [0.0, 1.0, 0.0, 0.0], // identity + [FRAC_PI_2, 1.0, 0.0, 0.0], // fourfold about 1 0 0 (x1) + [PI, 1.0, 0.0, 0.0], // + [FRAC_PI_2 * 3.0, 1.0, 0.0, 0.0], // + [FRAC_PI_2, 0.0, 1.0, 0.0], // fourfold about 0 1 0 (x2) + [PI, 0.0, 1.0, 0.0], // + [FRAC_PI_2 * 3.0, 0.0, 1.0, 0.0], // + [FRAC_PI_2, 0.0, 0.0, 1.0], // fourfold about 0 0 1 (x3) + [PI, 0.0, 0.0, 1.0], // + [FRAC_PI_2 * 3.0, 0.0, 0.0, 1.0], // + [FRAC_PI_3 * 2.0, 1.0, 1.0, 1.0], // threefold about 1 1 1 + [FRAC_PI_3 * 4.0, 1.0, 1.0, 1.0], // + [FRAC_PI_3 * 2.0, -1.0, 1.0, 1.0], // threefold about -1 1 1 + [FRAC_PI_3 * 4.0, -1.0, 1.0, 1.0], // + [FRAC_PI_3 * 2.0, -1.0, -1.0, 1.0], // threefold about -1 -1 1 + [FRAC_PI_3 * 4.0, -1.0, -1.0, 1.0], // + [FRAC_PI_3 * 2.0, 1.0, -1.0, 1.0], // threefold about 1 -1 1 + [FRAC_PI_3 * 4.0, 1.0, -1.0, 1.0], // + [PI, 1.0, 1.0, 0.0], // twofold about 1 1 0 + [PI, -1.0, 1.0, 0.0], // twofold about -1 1 0 + [PI, 1.0, 0.0, 1.0], // twofold about 1 0 1 + [PI, 0.0, 1.0, 1.0], // twofold about 0 1 1 + [PI, -1.0, 0.0, 1.0], // twofold about -1 0 1 + [PI, 0.0, -1.0, 1.0], // twofold about 0 -1 1 + ], + LatticeType::Hexagonal => vec![ + [0.0, 1.0, 0.0, 0.0], // identity + [FRAC_PI_3, 0.0, 0.0, 1.0], // sixfold about 0 0 1 (x3,c) + [FRAC_PI_3 * 2.0, 0.0, 0.0, 1.0], + [PI, 0.0, 0.0, 1.0], + [FRAC_PI_3 * 4.0, 0.0, 0.0, 1.0], + [FRAC_PI_3 * 5.0, 0.0, 0.0, 1.0], + [PI, 1.0, 0.0, 0.0], // twofold about 2 -1 0 (x1,a1) + [PI, -0.5, sqrt3_2, 0.0], // twofold about -1 2 0 (a2) + [PI, -0.5, -sqrt3_2, 0.0], // twofold about -1 -1 0 (a3) + [PI, sqrt3_2, 0.5, 0.0], // twofold about 1 0 0 + [PI, 0.0, 1.0, 0.0], // twofold about -1 1 0 (x2) + [PI, -sqrt3_2, 0.5, 0.0], // twofold about 0 -1 0 + ], + LatticeType::Trigonal => vec![ + [0.0, 1.0, 0.0, 0.0], // identity + [FRAC_PI_3 * 2.0, 0.0, 0.0, 1.0], // threefold about 0001 (x3,c) + [FRAC_PI_3 * 4.0, 0.0, 0.0, 1.0], + [PI, 1.0, 0.0, 0.0], // twofold about 2 -1 -1 0 (x1,a1) + [PI, -0.5, sqrt3_2, 0.0], // twofold about -1 2 -1 0 (a2) + [PI, -0.5, -sqrt3_2, 0.0], // twofold about -1 -1 2 0 (a3) + ], + LatticeType::Rhombohedral => vec![ + [0.0, 1.0, 0.0, 0.0], // identity + [FRAC_PI_3 * 2.0, 1.0, 1.0, 1.0], // threefold about [111] + [FRAC_PI_3 * 4.0, 1.0, 1.0, 1.0], + [PI, 2.0 * isqrt6, -isqrt6, -isqrt6], // twofold perpendicular to [111] + [PI, -isqrt6, 2.0 * isqrt6, -isqrt6], + [PI, -isqrt6, -isqrt6, 2.0 * isqrt6], + ], + LatticeType::Tetragonal => vec![ + [0.0, 1.0, 0.0, 0.0], // identity + [FRAC_PI_2, 0.0, 0.0, 1.0], // fourfold about 0 0 1 (x3) + [PI, 0.0, 0.0, 1.0], + [FRAC_PI_2 * 3.0, 0.0, 0.0, 1.0], + [PI, 1.0, 0.0, 0.0], // twofold about 1 0 0 (x1) + [PI, 0.0, 1.0, 0.0], // twofold about 0 1 0 (x2) + [PI, 1.0, 1.0, 0.0], // twofold about 1 1 0 + [PI, -1.0, 1.0, 0.0], // twofold about -1 1 0 + ], + LatticeType::Orthorhombic => vec![ + [0.0, 1.0, 0.0, 0.0], // identity + [PI, 1.0, 0.0, 0.0], // twofold about 1 0 0 + [PI, 0.0, 1.0, 0.0], // twofold about 0 1 0 + [PI, 0.0, 0.0, 1.0], // twofold about 0 0 1 + ], + LatticeType::Monoclinic => vec![ + [0.0, 1.0, 0.0, 0.0], // identity + [PI, 0.0, 1.0, 0.0], // twofold about 010 (x2) + ], + LatticeType::Triclinic => vec![ + [0.0, 1.0, 0.0, 0.0], // identity + ], + } + } + + /// Point group rotations as unit quaternions [w, x, y, z] with w >= 0 + pub fn symmetric_quaternions(&self) -> Vec<[f64; 4]> { + self.symmetry_angle_axis() + .iter() + .map(|ang_axis| { + let half = 0.5 * ang_axis[0]; + let s = f64::sin(half); + let inv_norm_axis = 1.0 / norm::<3, f64>(&ang_axis[1..4]); + let mut quat = [ + f64::cos(half), + s * ang_axis[1] * inv_norm_axis, + s * ang_axis[2] * inv_norm_axis, + s * ang_axis[3] * inv_norm_axis, + ]; + if quat[0] < 0.0 { + quat.iter_mut().for_each(|q| *q *= -1.0); + } + quat + }) + .collect() + } +} + +/// Computes the reciprocal lattice B from cell parameters [a, b, c, alpha, beta, gamma]. +/// Different from HEXRD we return it as a row matrix (each row is a reciprocal lattice +/// vector) as that's the easiest way of doing things, the same as ExaConstit's version. +pub fn compute_lattice_b_param(cellparms: &[f64; 6]) -> [[f64; 3]; 3] { + let alfa = cellparms[3]; + let beta = cellparms[4]; + let gamma = cellparms[5]; + + let cosalfar = + (f64::cos(beta) * f64::cos(gamma) - f64::cos(alfa)) / (f64::sin(beta) * f64::sin(gamma)); + let sinalfar = f64::sqrt(1.0_f64 - cosalfar * cosalfar); + + let a = [cellparms[0], 0.0_f64, 0.0_f64]; + let b = [ + cellparms[1] * f64::cos(gamma), + cellparms[1] * f64::sin(gamma), + 0.0_f64, + ]; + let c = [ + cellparms[2] * f64::cos(beta), + -cellparms[2] * cosalfar * f64::sin(beta), + cellparms[2] * sinalfar * f64::sin(beta), + ]; + + // Cell volume + let inv_vol = { + let v_temp = cross_prod(&b, &c); + 1.0 / dot_prod::<3, f64>(&a, &v_temp) + }; + + // Reciprocal lattice vectors + let cross_prod_inv_v = |vec1: &[f64], vec2: &[f64]| -> [f64; 3] { + let mut tmp = cross_prod(vec1, vec2); + tmp.iter_mut().for_each(|t| *t *= inv_vol); + tmp + }; + + // B takes components in the reciprocal lattice to X + [ + cross_prod_inv_v(&b, &c), + cross_prod_inv_v(&c, &a), + cross_prod_inv_v(&a, &b), + ] +} + +/// Direct lattice vectors (rows) for the same cell, used to check the reciprocal lattice +#[cfg(test)] +fn direct_lattice(cellparms: &[f64; 6]) -> [[f64; 3]; 3] { + let (alfa, beta, gamma) = (cellparms[3], cellparms[4], cellparms[5]); + let cosalfar = + (f64::cos(beta) * f64::cos(gamma) - f64::cos(alfa)) / (f64::sin(beta) * f64::sin(gamma)); + let sinalfar = f64::sqrt(1.0_f64 - cosalfar * cosalfar); + [ + [cellparms[0], 0.0, 0.0], + [ + cellparms[1] * f64::cos(gamma), + cellparms[1] * f64::sin(gamma), + 0.0, + ], + [ + cellparms[2] * f64::cos(beta), + -cellparms[2] * cosalfar * f64::sin(beta), + cellparms[2] * sinalfar * f64::sin(beta), + ], + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + fn quat_mult(p: &[f64; 4], q: &[f64; 4]) -> [f64; 4] { + [ + p[0] * q[0] - p[1] * q[1] - p[2] * q[2] - p[3] * q[3], + p[0] * q[1] + p[1] * q[0] + p[2] * q[3] - p[3] * q[2], + p[0] * q[2] - p[1] * q[3] + p[2] * q[0] + p[3] * q[1], + p[0] * q[3] + p[1] * q[2] - p[2] * q[1] + p[3] * q[0], + ] + } + + fn same_rotation(p: &[f64; 4], q: &[f64; 4]) -> bool { + let diff: f64 = (0..4).map(|i| (p[i] - q[i]).abs()).sum(); + let sum: f64 = (0..4).map(|i| (p[i] + q[i]).abs()).sum(); + diff < 1e-12 || sum < 1e-12 + } + + #[test] + fn symmetry_groups_are_closed_and_distinct() { + let expected = [24, 12, 6, 6, 8, 4, 2, 1]; + for (lt, nsym) in LatticeType::ALL.iter().zip(expected) { + let quats = lt.symmetric_quaternions(); + assert_eq!(quats.len(), nsym, "{:?}", lt); + for (i, p) in quats.iter().enumerate() { + let n = norm::<4, f64>(p); + assert!((n - 1.0).abs() < 1e-14, "{:?} op {} not unit", lt, i); + for q in quats.iter().skip(i + 1) { + assert!(!same_rotation(p, q), "{:?} has a duplicate operator", lt); + } + for q in quats.iter() { + let pq = quat_mult(p, q); + assert!( + quats.iter().any(|r| same_rotation(&pq, r)), + "{:?} is not closed under composition", + lt + ); + } + } + } + } + + #[test] + fn reciprocal_lattice_is_dual_to_direct_lattice() { + let params: [(LatticeType, Vec); 8] = [ + (LatticeType::Cubic, vec![3.6]), + (LatticeType::Hexagonal, vec![2.95, 4.68]), + (LatticeType::Trigonal, vec![4.76, 12.99]), + (LatticeType::Rhombohedral, vec![5.1, 0.95]), + (LatticeType::Tetragonal, vec![4.59, 2.96]), + (LatticeType::Orthorhombic, vec![2.85, 5.87, 4.96]), + (LatticeType::Monoclinic, vec![5.15, 5.21, 5.31, 1.73]), + ( + LatticeType::Triclinic, + vec![7.1, 7.8, 8.2, 1.62, 1.75, 1.45], + ), + ]; + for (lt, p) in params.iter() { + let cell = lt.cell_parameters(p).unwrap(); + let bmat = compute_lattice_b_param(&cell); + let amat = direct_lattice(&cell); + for (i, a_i) in amat.iter().enumerate() { + for (j, b_j) in bmat.iter().enumerate() { + let d = dot_prod::<3, f64>(a_i, b_j); + let expected = if i == j { 1.0 } else { 0.0 }; + assert!( + (d - expected).abs() < 1e-12, + "{:?} a_{} . b*_{} = {}", + lt, + i, + j, + d + ); + } + } + } + } + + #[test] + fn cubic_reciprocal_lattice_is_scaled_identity() { + let bmat = compute_lattice_b_param(&LatticeType::Cubic.cell_parameters(&[4.0]).unwrap()); + for (i, row) in bmat.iter().enumerate() { + for (j, val) in row.iter().enumerate() { + let expected = if i == j { 0.25 } else { 0.0 }; + assert!((val - expected).abs() < 1e-15); + } + } + } + + #[test] + fn parameter_validation() { + assert!(LatticeType::from_name("Hexagonal").is_ok()); + assert!(LatticeType::from_name("hcp").is_err()); + assert!(LatticeType::Hexagonal.cell_parameters(&[2.95]).is_err()); + assert!(LatticeType::Cubic.cell_parameters(&[-3.6]).is_err()); + assert!(LatticeType::Cubic.cell_parameters(&[3.6]).is_ok()); + } +} diff --git a/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/math.rs b/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/math.rs index dea75cf..ac13656 100644 --- a/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/math.rs +++ b/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/math.rs @@ -1,3 +1,9 @@ +//! Small fixed-size dense linear algebra helpers (dot / cross products, norms, +//! matrix-vector and matrix-matrix products, tensor rotation) used by the light-up +//! calculations. Matrices are row-major arrays of rows (`[[F; NCOLS]]`), and the +//! dimensions are compile time constants so everything stays on the stack. +//! Not every helper is used by the Python bindings, hence the dead_code allowance. + #![allow(dead_code)] use libnum::{Float, NumAssignOps, NumOps, One, Zero}; @@ -64,9 +70,9 @@ pub fn norm_column( } // Accumulate the results across all remaining rows - for i_n in 1..NDIM { + for row in matrix.iter().take(NDIM).skip(1) { for j_m in 0..MDIM { - norm_vec[j_m] += matrix[i_n][j_m] * matrix[i_n][j_m]; + norm_vec[j_m] += row[j_m] * row[j_m]; } } @@ -403,4 +409,4 @@ pub fn rotate_matrix( } } } -} \ No newline at end of file +} diff --git a/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/mod.rs b/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/mod.rs index 717ca96..d60bd02 100644 --- a/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/mod.rs +++ b/scripts/postprocessing/xtal_light_up/src/pyxtal_light_up/mod.rs @@ -1,336 +1,566 @@ +//! Python bindings for computing lattice strain / "light-up" quantities from +//! ExaConstit results, mirroring ExaConstit's in-situ light-up calculations +//! (`LightUp` in src/postprocessing/mechanics_lightup.cpp) so the two produce the +//! same results for every supported crystal system. +//! +//! The usual workflow (see light_up_py/fiber_calcs_rank.py) is: +//! 1. calc_within_fibers to flag which elements are in each HKL fiber +//! 2. calc_lattice_strains, calc_directional_stiffness_lattice_fiber and +//! calc_taylor_factors_lattice_fiber to get the fiber (volume) averaged values +//! +//! The non-fiber versions (calc_directional_stiffness, calc_taylor_factors) give +//! the same per-element quantities without any fiber averaging. + use numpy::{PyArray1, PyArray2, PyArray3, PyArrayMethods}; -use pyo3::{pymodule, Bound, types::PyModule, PyResult, Python}; - +use pyo3::prelude::PyModuleMethods; +use pyo3::{pyfunction, pymodule, types::PyModule, wrap_pyfunction, Bound, FromPyObject, PyResult}; -// use crate::xtal_light_up; +pub(crate) mod lattice; pub(crate) mod math; +use lattice::{compute_lattice_b_param, LatticeType}; use math::*; -#[pymodule] -fn xtal_light_up(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - #[pyfn(m)] - #[pyo3(name = "strain_lattice2sample")] - fn strain_lattice2sample<'py>( - lattice_orientations: &Bound<'_, PyArray3>, - strains: &Bound<'_, PyArray3>, - ) -> anyhow::Result<()> { - let xtal_o = lattice_orientations.readonly(); - let xtal_orientations = xtal_o.as_slice()?; - let mut xtal_s = strains.readwrite(); - let xtal_strains = xtal_s.as_slice_mut()?; - - // let xtal_ori_chunks = xtal_orientations.chunks_exact(4); - // let mut xtal_strain_chunks = xtal_strains.chunks_exact_mut(6); - // zip(xtal_orientations.chunks_exact(4)). - // |(strain, _quats)| - // xtal_strains.par_chunks_exact_mut(6).enumerate().for_each(|(i, strain)| { strain[0] = 0.0_f64; - // // Zipping quats isn't the most obvious if we wanted chunks of things - // // in parallel at least... - // // This at least gets us around the compiler issue - // let quats : &[f64] = { - // let start = i * 4; - // let end = (i + 1) * 4; - // &xtal_orientations[start..end] - // }; - // }); - - xtal_strains.chunks_exact_mut(6).zip(xtal_orientations.chunks_exact(4)).for_each(|(strain_vec, quat)| { - rotate_strain_to_sample(quat, strain_vec); +/// Rotates strains from the lattice (crystal) frame into the sample frame in place. +/// +/// Arguments: +/// - lattice_orientations: (steps, elems, 4) quaternions for each element +/// - strains: (steps, elems, 6) lattice frame strains, overwritten with the sample +/// frame strains +/// +/// ExaConstit v0.9+ already writes its elastic strains in the sample frame, so this +/// is only needed for older results. +#[pyfunction] +#[pyo3(name = "strain_lattice2sample")] +fn strain_lattice2sample( + lattice_orientations: &Bound<'_, PyArray3>, + strains: &Bound<'_, PyArray3>, +) -> anyhow::Result<()> { + let xtal_o = lattice_orientations.readonly(); + let xtal_orientations = xtal_o.as_slice()?; + let mut xtal_s = strains.readwrite(); + let xtal_strains = xtal_s.as_slice_mut()?; + + // let xtal_ori_chunks = xtal_orientations.chunks_exact(4); + // let mut xtal_strain_chunks = xtal_strains.chunks_exact_mut(6); + // zip(xtal_orientations.chunks_exact(4)). + // |(strain, _quats)| + // xtal_strains.par_chunks_exact_mut(6).enumerate().for_each(|(i, strain)| { strain[0] = 0.0_f64; + // // Zipping quats isn't the most obvious if we wanted chunks of things + // // in parallel at least... + // // This at least gets us around the compiler issue + // let quats : &[f64] = { + // let start = i * 4; + // let end = (i + 1) * 4; + // &xtal_orientations[start..end] + // }; + // }); + + xtal_strains + .as_chunks_mut::<6>() + .0 + .iter_mut() + .zip(xtal_orientations.as_chunks::<4>().0) + .for_each(|(strain_vec, quat)| { + rotate_strain_to_sample(quat, strain_vec); }); - Ok(()) - } - + Ok(()) +} - #[pyfn(m)] - #[pyo3(name = "calc_taylor_factors")] - fn calc_taylor_factors<'py>( - shear_rates: &Bound<'_, PyArray3>, - taylor_factors: &Bound<'_, PyArray2>, - eff_plastic_def_rate: &Bound<'_, PyArray2>, - ) -> anyhow::Result<()> { - let gamma_dots = shear_rates.readonly(); - let gammadots = gamma_dots.as_slice()?; - let mut tay_factors = taylor_factors.readwrite(); - let tayfacs = tay_factors.as_slice_mut()?; - let mut eff_pl_def_rate = eff_plastic_def_rate.readwrite(); - let eps_rate = eff_pl_def_rate.as_slice_mut()?; - - let eps_rate_gdots = eps_rate.iter_mut().zip(gammadots.chunks_exact(12)); - tayfacs.iter_mut().zip(eps_rate_gdots).for_each(|(tayfac, eps_rate_gdot)| { - (*tayfac, *eps_rate_gdot.0) = calc_taylor_factor(eps_rate_gdot.1); +/// Taylor factor for every element: sum over the slip systems of |shear rate| +/// divided by the equivalent plastic strain rate, the same way ExaConstit's in-situ +/// light-up computes it. Elements with an equivalent plastic strain rate of +/// essentially zero (<= 1e-14) get a Taylor factor of 0. +/// +/// Arguments: +/// - shear_rates: (steps, elems, nslip) slip system shear rates ("Shearing Rate" +/// output); any number of slip systems works +/// - eq_pl_strain_rate: (steps, elems) ExaCMech's equivalent plastic strain rate +/// ("Equivalent Plastic Strain Rate" output) +/// - taylor_factors: (steps, elems) output, overwritten +#[pyfunction] +#[pyo3(name = "calc_taylor_factors")] +fn calc_taylor_factors( + shear_rates: &Bound<'_, PyArray3>, + eq_pl_strain_rate: &Bound<'_, PyArray2>, + taylor_factors: &Bound<'_, PyArray2>, +) -> anyhow::Result<()> { + let num_slip = shear_rates.readonly().as_array().dim().2; + let gamma_dots = shear_rates.readonly(); + let gammadots = gamma_dots.as_slice()?; + let eq_pl_rate = eq_pl_strain_rate.readonly(); + let dpeffs = eq_pl_rate.as_slice()?; + let mut tay_factors = taylor_factors.readwrite(); + let tayfacs = tay_factors.as_slice_mut()?; + + check_taylor_inputs(num_slip, gammadots.len(), dpeffs.len(), tayfacs.len())?; + + tayfacs + .iter_mut() + .zip(gammadots.chunks_exact(num_slip)) + .zip(dpeffs) + .for_each(|((tayfac, gdots), dpeff)| { + *tayfac = calc_taylor_factor(gdots, *dpeff); }); - Ok(()) - } - - #[pyfn(m)] - #[pyo3(name = "calc_directional_stiffness")] - fn calc_directional_stiffness<'py>( - cauchy_stress: &Bound<'_, PyArray3>, - sample_strain: &Bound<'_, PyArray3>, - directional_stiffness: &Bound<'_, PyArray2>, - ) -> anyhow::Result<()> { - - let cauchy = cauchy_stress.readonly(); - let stress = cauchy.as_slice()?; - let sstrain = sample_strain.readonly(); - let strain = sstrain.as_slice()?; - - let mut dir_stiffness = directional_stiffness.readwrite(); - let dir_stiff = dir_stiffness.as_slice_mut()?; - - let stress_strains = stress.chunks_exact(6).zip(strain.chunks_exact(6)); + Ok(()) +} - dir_stiff.iter_mut().zip(stress_strains).for_each(|(dir_stiff, stress_strains)| { +/// Directional stiffness for every element: the sample frame 33 (z) Cauchy stress +/// divided by the sample frame 33 elastic strain. Elements whose 33 strain is +/// essentially zero (< machine epsilon) get a value of 0. +/// +/// Arguments: +/// - cauchy_stress: (steps, elems, 6) sample frame Cauchy stress +/// - sample_strain: (steps, elems, 6) sample frame elastic strain for the same steps +/// - directional_stiffness: (steps, elems) output, overwritten +#[pyfunction] +#[pyo3(name = "calc_directional_stiffness")] +fn calc_directional_stiffness( + cauchy_stress: &Bound<'_, PyArray3>, + sample_strain: &Bound<'_, PyArray3>, + directional_stiffness: &Bound<'_, PyArray2>, +) -> anyhow::Result<()> { + let cauchy = cauchy_stress.readonly(); + let stress = cauchy.as_slice()?; + let sstrain = sample_strain.readonly(); + let strain = sstrain.as_slice()?; + + let mut dir_stiffness = directional_stiffness.readwrite(); + let dir_stiff = dir_stiffness.as_slice_mut()?; + + let stress_strains = stress + .as_chunks::<6>() + .0 + .iter() + .zip(strain.as_chunks::<6>().0); + + dir_stiff + .iter_mut() + .zip(stress_strains) + .for_each(|(dir_stiff, stress_strains)| { if f64::abs(stress_strains.1[2]) < f64::EPSILON { *dir_stiff = 0.0_f64 - } - else { + } else { *dir_stiff = stress_strains.0[2] / stress_strains.1[2]; } }); - Ok(()) - } + Ok(()) +} - #[pyfn(m)] - #[pyo3(name = "calc_lattice_strains")] - fn calc_lattice_strains<'py>( - sample_strains: &Bound<'_, PyArray3>, - sample_dir: &Bound<'_, PyArray1>, - elem_vols: &Bound<'_, PyArray2>, - in_fibers: &Bound<'_, PyArray3>, - lattice_strains: &Bound<'_, PyArray2>, - lattice_volumes: &Bound<'_, PyArray2>, - per_rank_update: bool - ) -> anyhow::Result<()> { - let num_elems: usize; - let num_steps: usize; - - let s_in_fibers = in_fibers.readonly(); - { - let in_fiber_av = s_in_fibers.as_array(); - (_, num_steps, num_elems) = in_fiber_av.dim(); - } +/// Lattice strain for each HKL fiber: the sample frame elastic strain projected onto +/// the sample direction (s . strain . s), volume weighted over the elements in the +/// fiber. +/// +/// Arguments: +/// - sample_strains: (steps, elems, 6) sample frame elastic strains +/// - sample_dir: (3,) sample direction, normalized internally +/// - elem_vols: (steps, elems) element volumes +/// - in_fibers: (nhkl, steps, elems) from calc_within_fibers +/// - lattice_strains: (nhkl, steps) output, added to +/// - lattice_volumes: (nhkl, steps) output, added to; the total in-fiber volume +/// - per_rank_update: if true the volume weighted sums are added to the outputs +/// without normalizing, so results from several ExaConstit ranks / data blocks can +/// be accumulated, and you divide lattice_strains by lattice_volumes once at the +/// end. If false each call adds the volume average for just its own elements. +#[pyfunction] +#[pyo3(name = "calc_lattice_strains")] +fn calc_lattice_strains( + sample_strains: &Bound<'_, PyArray3>, + sample_dir: &Bound<'_, PyArray1>, + elem_vols: &Bound<'_, PyArray2>, + in_fibers: &Bound<'_, PyArray3>, + lattice_strains: &Bound<'_, PyArray2>, + lattice_volumes: &Bound<'_, PyArray2>, + per_rank_update: bool, +) -> anyhow::Result<()> { + let num_elems: usize; + let num_steps: usize; + + let s_in_fibers = in_fibers.readonly(); + { + let in_fiber_av = s_in_fibers.as_array(); + (_, num_steps, num_elems) = in_fiber_av.dim(); + } - let in_fiber_hkls = s_in_fibers.as_slice()?; - let s_strains = sample_strains.readonly(); - let strains = s_strains.as_slice()?; + let in_fiber_hkls = s_in_fibers.as_slice()?; + let s_strains = sample_strains.readonly(); + let strains = s_strains.as_slice()?; - let mut s_lattice_strains = lattice_strains.readwrite(); - let lat_strains = s_lattice_strains.as_slice_mut()?; + let mut s_lattice_strains = lattice_strains.readwrite(); + let lat_strains = s_lattice_strains.as_slice_mut()?; - let mut s_lattice_vols = lattice_volumes.readwrite(); - let lat_vols = s_lattice_vols.as_slice_mut()?; + let mut s_lattice_vols = lattice_volumes.readwrite(); + let lat_vols = s_lattice_vols.as_slice_mut()?; - let s_elem_vols = elem_vols.readonly(); - let evs = s_elem_vols.as_slice()?; + let s_elem_vols = elem_vols.readonly(); + let evs = s_elem_vols.as_slice()?; - let s_sample_dir = sample_dir.readonly(); - let s_dir = s_sample_dir.as_slice()?; + let s_sample_dir = sample_dir.readonly(); + let s_dir = s_sample_dir.as_slice()?; - let project_vec = [s_dir[0] * s_dir[0], s_dir[1] * s_dir[1] , s_dir[2] * s_dir[2] , 2.0_f64 * s_dir[1] * s_dir[2] , 2.0_f64 * s_dir[0] * s_dir[2] , 2.0_f64 * s_dir[0] * s_dir[1]]; + let project_vec = [ + s_dir[0] * s_dir[0], + s_dir[1] * s_dir[1], + s_dir[2] * s_dir[2], + 2.0_f64 * s_dir[1] * s_dir[2], + 2.0_f64 * s_dir[0] * s_dir[2], + 2.0_f64 * s_dir[0] * s_dir[1], + ]; - let lat_strain_in_fiber_iter = lat_strains.chunks_exact_mut(num_steps).zip(in_fiber_hkls.chunks_exact(num_steps * num_elems)); + let lat_strain_in_fiber_iter = lat_strains + .chunks_exact_mut(num_steps) + .zip(in_fiber_hkls.chunks_exact(num_steps * num_elems)); - let lat_vol_strain_in_fiber_iter = lat_vols.chunks_exact_mut(num_steps).zip(lat_strain_in_fiber_iter); + let lat_vol_strain_in_fiber_iter = lat_vols + .chunks_exact_mut(num_steps) + .zip(lat_strain_in_fiber_iter); - for (lat_vols_hkl, (lat_strains_hkl, in_fiber_hkl)) in lat_vol_strain_in_fiber_iter.into_iter() { - let ev_strains_iter = evs.chunks_exact(num_elems).zip(strains.chunks_exact(num_elems * 6)); - let ev_strains_in_fiber_iter = in_fiber_hkl.chunks_exact(num_elems).zip(ev_strains_iter); + for (lat_vols_hkl, (lat_strains_hkl, in_fiber_hkl)) in lat_vol_strain_in_fiber_iter.into_iter() + { + let ev_strains_iter = evs + .chunks_exact(num_elems) + .zip(strains.chunks_exact(num_elems * 6)); + let ev_strains_in_fiber_iter = in_fiber_hkl.chunks_exact(num_elems).zip(ev_strains_iter); - for (istep, (in_fiber_hkl_step, (evs_step, strains_step))) in ev_strains_in_fiber_iter.enumerate() { - let (total_lat_vol, inv_total_lat_vol) = calc_volume_terms_fiber(evs_step, in_fiber_hkl_step, per_rank_update); + for (istep, (in_fiber_hkl_step, (evs_step, strains_step))) in + ev_strains_in_fiber_iter.enumerate() + { + let (total_lat_vol, inv_total_lat_vol) = + calc_volume_terms_fiber(evs_step, in_fiber_hkl_step, per_rank_update); - lat_vols_hkl[istep] += total_lat_vol; + lat_vols_hkl[istep] += total_lat_vol; - lat_strains_hkl[istep] += strains_step.chunks_exact(6) + lat_strains_hkl[istep] += strains_step + .as_chunks::<6>() + .0 + .iter() .zip(evs_step) .zip(in_fiber_hkl_step) .filter(|ev_strain_in_fiber| *ev_strain_in_fiber.1) .map(|((strain, ev), _)| { - ev * inv_total_lat_vol * dot_prod::<6,f64>(&project_vec, strain) + ev * inv_total_lat_vol * dot_prod::<6, f64>(&project_vec, strain) }) - .reduce(|lat_strain, lat_strain_elem| - lat_strain + lat_strain_elem - ).unwrap_or(0.0_f64); - } + .reduce(|lat_strain, lat_strain_elem| lat_strain + lat_strain_elem) + .unwrap_or(0.0_f64); } - Ok(()) } + Ok(()) +} - #[pyfn(m)] - #[pyo3(name = "calc_taylor_factors_lattice_fiber")] - fn calc_taylor_factors_lattice_fiber<'py>( - shear_rates: &Bound<'_, PyArray3>, - taylor_factors: &Bound<'_, PyArray2>, - eff_plastic_def_rate: &Bound<'_, PyArray2>, - elem_vols: &Bound<'_, PyArray2>, - in_fibers: &Bound<'_, PyArray3>, - per_rank_update: bool - ) -> anyhow::Result<()> { - let num_elems: usize; - let num_steps: usize; - - let s_in_fibers = in_fibers.readonly(); +/// Volume weighted Taylor factor and equivalent plastic strain rate for each HKL +/// fiber, mirroring LightUp::CalcLatticeTaylorFactorDpeff in ExaConstit. See +/// calc_taylor_factors for how the per-element Taylor factor is computed. +/// +/// Arguments: +/// - shear_rates: (steps, elems, nslip) slip system shear rates; any number of slip +/// systems works +/// - eq_pl_strain_rate: (steps, elems) ExaCMech's equivalent plastic strain rate +/// - taylor_factors: (nhkl, steps) output, added to +/// - lattice_eq_pl_strain_rate: (nhkl, steps) output, added to +/// - elem_vols: (steps, elems) element volumes for the same steps +/// - in_fibers: (nhkl, steps, elems) from calc_within_fibers for the same steps +/// - per_rank_update: see calc_lattice_strains; when true divide the outputs by the +/// in-fiber volumes (lattice_volumes) once everything has been accumulated +#[pyfunction] +#[pyo3(name = "calc_taylor_factors_lattice_fiber")] +fn calc_taylor_factors_lattice_fiber( + shear_rates: &Bound<'_, PyArray3>, + eq_pl_strain_rate: &Bound<'_, PyArray2>, + taylor_factors: &Bound<'_, PyArray2>, + lattice_eq_pl_strain_rate: &Bound<'_, PyArray2>, + elem_vols: &Bound<'_, PyArray2>, + in_fibers: &Bound<'_, PyArray3>, + per_rank_update: bool, +) -> anyhow::Result<()> { + let num_slip = shear_rates.readonly().as_array().dim().2; + let num_elems: usize; + let num_steps: usize; + + let s_in_fibers = in_fibers.readonly(); + { + let in_fiber_av = s_in_fibers.as_array(); + (_, num_steps, num_elems) = in_fiber_av.dim(); + } + let in_fiber_hkls = s_in_fibers.as_slice()?; + let s_elem_vols = elem_vols.readonly(); + let evs = s_elem_vols.as_slice()?; + + let gamma_dots = shear_rates.readonly(); + let gammadots = gamma_dots.as_slice()?; + let mut tay_factors = taylor_factors.readwrite(); + let tayfacs = tay_factors.as_slice_mut()?; + let mut lat_eq_pl_rate = lattice_eq_pl_strain_rate.readwrite(); + let eps_rate = lat_eq_pl_rate.as_slice_mut()?; + let eq_pl_rate = eq_pl_strain_rate.readonly(); + let dpeffs = eq_pl_rate.as_slice()?; + + check_taylor_inputs(num_slip, gammadots.len(), dpeffs.len(), evs.len())?; + + let lat_eps_in_fiber_iter = eps_rate + .chunks_exact_mut(num_steps) + .zip(in_fiber_hkls.chunks_exact(num_steps * num_elems)); + let ltf_leps_in_fiber_iter = tayfacs + .chunks_exact_mut(num_steps) + .zip(lat_eps_in_fiber_iter); + + for (tay_fac_hkl, (eps_rate_hkl, in_fiber_hkl)) in ltf_leps_in_fiber_iter.into_iter() { + let ev_gdot_iter = evs + .chunks_exact(num_elems) + .zip(gammadots.chunks_exact(num_elems * num_slip)); + let ev_gdot_in_fiber_iter = in_fiber_hkl.chunks_exact(num_elems).zip(ev_gdot_iter); + for (istep, (in_fiber_hkl_step, (evs_step, gdots_step))) in + ev_gdot_in_fiber_iter.enumerate() { - let in_fiber_av = s_in_fibers.as_array(); - (_, num_steps, num_elems) = in_fiber_av.dim(); - } - let in_fiber_hkls = s_in_fibers.as_slice()?; - let s_elem_vols = elem_vols.readonly(); - let evs = s_elem_vols.as_slice()?; - - let gamma_dots = shear_rates.readonly(); - let gammadots = gamma_dots.as_slice()?; - let mut tay_factors = taylor_factors.readwrite(); - let tayfacs = tay_factors.as_slice_mut()?; - let mut eff_pl_def_rate = eff_plastic_def_rate.readwrite(); - let eps_rate = eff_pl_def_rate.as_slice_mut()?; - - let lat_eps_in_fiber_iter = eps_rate.chunks_exact_mut(num_steps).zip(in_fiber_hkls.chunks_exact(num_steps * num_elems)); - let ltf_leps_in_fiber_iter = tayfacs.chunks_exact_mut(num_steps).zip(lat_eps_in_fiber_iter); - - for (tay_fac_hkl, (eps_rate_hkl, in_fiber_hkl)) in ltf_leps_in_fiber_iter.into_iter() { - let ev_gdot_iter = evs.chunks_exact(num_elems).zip(gammadots.chunks_exact(num_elems * 12)); - let ev_gdot_in_fiber_iter = in_fiber_hkl.chunks_exact(num_elems).zip(ev_gdot_iter); - for (istep, (in_fiber_hkl_step, (evs_step, gdots_step))) in ev_gdot_in_fiber_iter.enumerate() { - let (_, inv_total_lat_vol) = calc_volume_terms_fiber(evs_step, in_fiber_hkl_step, per_rank_update); - - let (tay_fac_hkl_step, eps_rate_hkl_step) = gdots_step.chunks_exact(12) + let (_, inv_total_lat_vol) = + calc_volume_terms_fiber(evs_step, in_fiber_hkl_step, per_rank_update); + let dpeff_step = &dpeffs[istep * num_elems..(istep + 1) * num_elems]; + + let (tay_fac_hkl_step, eps_rate_hkl_step) = gdots_step + .chunks_exact(num_slip) .zip(evs_step) .zip(in_fiber_hkl_step) - .filter(|ev_gdot_in_fiber| *ev_gdot_in_fiber.1) - .map(|((gdot, ev), _)| { - let (tf, deps) = calc_taylor_factor(gdot); + .enumerate() + .filter(|(_, ev_gdot_in_fiber)| *ev_gdot_in_fiber.1) + .map(|(ielem, ((gdot, ev), _))| { + let deps = dpeff_step[ielem]; + let tf = calc_taylor_factor(gdot, deps); (ev * inv_total_lat_vol * tf, ev * inv_total_lat_vol * deps) }) - .reduce(|tfdeps, tfdeps_elem| - (tfdeps.0 + tfdeps_elem.0, tfdeps.1 + tfdeps_elem.1) - ).unwrap_or((0.0_f64, 0.0_f64)); - tay_fac_hkl[istep] += tay_fac_hkl_step; - eps_rate_hkl[istep] += eps_rate_hkl_step; - } + .reduce(|tfdeps, tfdeps_elem| (tfdeps.0 + tfdeps_elem.0, tfdeps.1 + tfdeps_elem.1)) + .unwrap_or((0.0_f64, 0.0_f64)); + tay_fac_hkl[istep] += tay_fac_hkl_step; + eps_rate_hkl[istep] += eps_rate_hkl_step; } - Ok(()) } + Ok(()) +} - #[pyfn(m)] - #[pyo3(name = "calc_directional_stiffness_lattice_fiber")] - fn calc_directional_stiffness_lattice_fiber<'py>( - cauchy_stress: &Bound<'_, PyArray3>, - sample_strain: &Bound<'_, PyArray3>, - directional_stiffness: &Bound<'_, PyArray2>, - elem_vols: &Bound<'_, PyArray2>, - in_fibers: &Bound<'_, PyArray3>, - per_rank_update: bool - ) -> anyhow::Result<()> { - let num_elems: usize; - let num_steps: usize; - - let s_in_fibers = in_fibers.readonly(); +/// Volume weighted directional stiffness (sample frame 33 Cauchy stress / 33 +/// elastic strain, see calc_directional_stiffness) for each HKL fiber. +/// +/// Arguments: +/// - cauchy_stress: (steps, elems, 6) sample frame Cauchy stress +/// - sample_strain: (steps, elems, 6) sample frame elastic strain for the same steps +/// - directional_stiffness: (nhkl, steps) output, added to +/// - elem_vols: (steps, elems) element volumes for the same steps +/// - in_fibers: (nhkl, steps, elems) from calc_within_fibers for the same steps +/// - per_rank_update: see calc_lattice_strains +#[pyfunction] +#[pyo3(name = "calc_directional_stiffness_lattice_fiber")] +fn calc_directional_stiffness_lattice_fiber( + cauchy_stress: &Bound<'_, PyArray3>, + sample_strain: &Bound<'_, PyArray3>, + directional_stiffness: &Bound<'_, PyArray2>, + elem_vols: &Bound<'_, PyArray2>, + in_fibers: &Bound<'_, PyArray3>, + per_rank_update: bool, +) -> anyhow::Result<()> { + let num_elems: usize; + let num_steps: usize; + + let s_in_fibers = in_fibers.readonly(); + { + let in_fiber_av = s_in_fibers.as_array(); + (_, num_steps, num_elems) = in_fiber_av.dim(); + } + let in_fiber_hkls = s_in_fibers.as_slice()?; + let s_elem_vols = elem_vols.readonly(); + let evs = s_elem_vols.as_slice()?; + + let cauchy = cauchy_stress.readonly(); + let stress = cauchy.as_slice()?; + let sstrain = sample_strain.readonly(); + let strain = sstrain.as_slice()?; + + let mut dir_stiffness = directional_stiffness.readwrite(); + let dir_stiff = dir_stiffness.as_slice_mut()?; + + let lat_ds_in_fiber_iter = dir_stiff + .chunks_exact_mut(num_steps) + .zip(in_fiber_hkls.chunks_exact(num_steps * num_elems)); + + for (dir_stiff_hkl, in_fiber_hkl) in lat_ds_in_fiber_iter.into_iter() { + let stress_strains = stress + .chunks_exact(num_elems * 6) + .zip(strain.chunks_exact(num_elems * 6)); + let ev_sst_iter = evs.chunks_exact(num_elems).zip(stress_strains); + let ev_sst_in_fiber_iter = in_fiber_hkl.chunks_exact(num_elems).zip(ev_sst_iter); + for (istep, (in_fiber_hkl_step, (evs_step, (stress_step, strain_step)))) in + ev_sst_in_fiber_iter.enumerate() { - let in_fiber_av = s_in_fibers.as_array(); - (_, num_steps, num_elems) = in_fiber_av.dim(); - } - let in_fiber_hkls = s_in_fibers.as_slice()?; - let s_elem_vols = elem_vols.readonly(); - let evs = s_elem_vols.as_slice()?; - - let cauchy = cauchy_stress.readonly(); - let stress = cauchy.as_slice()?; - let sstrain = sample_strain.readonly(); - let strain = sstrain.as_slice()?; - - let mut dir_stiffness = directional_stiffness.readwrite(); - let dir_stiff = dir_stiffness.as_slice_mut()?; - - let lat_ds_in_fiber_iter = dir_stiff.chunks_exact_mut(num_steps).zip(in_fiber_hkls.chunks_exact(num_steps * num_elems)); - - for (dir_stiff_hkl, in_fiber_hkl) in lat_ds_in_fiber_iter.into_iter() { - let stress_strains = stress.chunks_exact(num_elems * 6).zip(strain.chunks_exact(num_elems * 6)); - let ev_sst_iter = evs.chunks_exact(num_elems).zip(stress_strains); - let ev_sst_in_fiber_iter = in_fiber_hkl.chunks_exact(num_elems).zip(ev_sst_iter); - for (istep, (in_fiber_hkl_step, (evs_step, (stress_step, strain_step)))) in ev_sst_in_fiber_iter.enumerate() { - let (_, inv_total_lat_vol) = calc_volume_terms_fiber(evs_step, in_fiber_hkl_step, per_rank_update); - - dir_stiff_hkl[istep] += stress_step.chunks_exact(6) - .zip(strain_step.chunks_exact(6)) + let (_, inv_total_lat_vol) = + calc_volume_terms_fiber(evs_step, in_fiber_hkl_step, per_rank_update); + + dir_stiff_hkl[istep] += stress_step + .as_chunks::<6>() + .0 + .iter() + .zip(strain_step.as_chunks::<6>().0) .zip(evs_step) .zip(in_fiber_hkl_step) .filter(|ev_sst_in_fiber| *ev_sst_in_fiber.1) .map(|((sst, ev), _)| { if f64::abs(sst.1[2]) < f64::EPSILON { 0.0_f64 - } - else { + } else { (sst.0[2] / sst.1[2]) * ev * inv_total_lat_vol } }) - .reduce(|dir_modulus, dir_modulus_elem| - dir_modulus + dir_modulus_elem - ).unwrap_or(0.0_f64); - } + .reduce(|dir_modulus, dir_modulus_elem| dir_modulus + dir_modulus_elem) + .unwrap_or(0.0_f64); } - - Ok(()) } - #[pyfn(m)] - #[pyo3(name = "calc_within_fibers")] - fn calc_within_fibers<'py>( - lattice_orientations: &Bound<'_, PyArray3>, - sample_dir: &Bound<'_, PyArray1>, - hkls: &Bound<'_, PyArray2>, - lattice_param_a: f64, - distance_tolerance_rad: f64, - in_fibers: &Bound<'_, PyArray3>, - ) -> anyhow::Result<()> { - - let num_elems: usize; - let num_steps: usize; - - let mut s_in_fibers = in_fibers.readwrite(); - { - let in_fiber_av = s_in_fibers.as_array(); - (_, num_steps, num_elems) = in_fiber_av.dim(); + Ok(()) +} + +/// Lattice parameters can be given as a single value (e.g. the cubic lattice +/// parameter, which is how this function was originally called) or as a list. +#[derive(FromPyObject)] +enum LatticeParams { + Scalar(f64), + List(Vec), +} + +impl LatticeParams { + fn to_vec(&self) -> Vec { + match self { + LatticeParams::Scalar(a) => vec![*a], + LatticeParams::List(params) => params.clone(), } + } +} - let in_fiber_hkls = s_in_fibers.as_slice_mut()?; +/// Flags which elements are "in fiber" for each HKL: an element is in the fiber when +/// one of the symmetrically equivalent crystal directions of the HKL plane normal, +/// once rotated by the element's orientation, lies within distance_tolerance_rad of +/// the sample direction. Mirrors LightUp::CalculateInFibers in ExaConstit. +/// +/// Arguments: +/// - lattice_orientations: (steps, elems, 4) quaternions for each element +/// - sample_dir: (3,) sample direction, normalized internally +/// - hkls: (nhkl, 3) Miller indices of the planes (3-index form, also for +/// hexagonal / trigonal) +/// - lattice_params: lattice parameter(s), a single value or a list; see below +/// - distance_tolerance_rad: angular tolerance in radians +/// - in_fibers: (nhkl, steps, elems) bool output, overwritten +/// - lattice_type: crystal system, default "cubic" +/// +/// lattice_type uses the same names as ExaConstit's `laue_type` option and +/// lattice_params the same parameters as its `lattice_params` option, with any +/// angles in radians: +/// - cubic -> a +/// - hexagonal / trigonal / tetragonal -> a, c +/// - rhombohedral -> a, alpha +/// - orthorhombic -> a, b, c +/// - monoclinic -> a, b, c, beta +/// - triclinic -> a, b, c, alpha, beta, gamma +/// +/// The wrong number of parameters, non-positive parameters or an unknown lattice +/// type raise an error. +#[pyfunction] +#[pyo3( + name = "calc_within_fibers", + signature = (lattice_orientations, sample_dir, hkls, lattice_params, distance_tolerance_rad, in_fibers, lattice_type = "cubic") +)] +fn calc_within_fibers( + lattice_orientations: &Bound<'_, PyArray3>, + sample_dir: &Bound<'_, PyArray1>, + hkls: &Bound<'_, PyArray2>, + lattice_params: LatticeParams, + distance_tolerance_rad: f64, + in_fibers: &Bound<'_, PyArray3>, + lattice_type: &str, +) -> anyhow::Result<()> { + let lat_type = LatticeType::from_name(lattice_type)?; + let cell_params = lat_type.cell_parameters(&lattice_params.to_vec())?; + // Computes reciprocal lattice B but different from HEXRD we return as row matrix as that's the easiest way of doing things + let lat_vec_ops_b = compute_lattice_b_param(&cell_params); + let symm_quat = lat_type.symmetric_quaternions(); - let xtal_o = lattice_orientations.readonly(); - let xtal_orientations = xtal_o.as_slice()?; + let num_elems: usize; + let num_steps: usize; - let s_hkls = hkls.readonly(); - let slice_hkls = s_hkls.as_slice()?; + let mut s_in_fibers = in_fibers.readwrite(); + { + let in_fiber_av = s_in_fibers.as_array(); + (_, num_steps, num_elems) = in_fiber_av.dim(); + } - let s_sample_dir = sample_dir.readonly(); - let s_dir = s_sample_dir.as_slice()?; + let in_fiber_hkls = s_in_fibers.as_slice_mut()?; - in_fiber_hkls.chunks_exact_mut(num_steps * num_elems) - .zip(slice_hkls.chunks_exact(3)) + let xtal_o = lattice_orientations.readonly(); + let xtal_orientations = xtal_o.as_slice()?; + + let s_hkls = hkls.readonly(); + let slice_hkls = s_hkls.as_slice()?; + + let s_sample_dir = sample_dir.readonly(); + let s_dir = s_sample_dir.as_slice()?; + + in_fiber_hkls + .chunks_exact_mut(num_steps * num_elems) + .zip(slice_hkls.as_chunks::<3>().0) .for_each(|(in_fibers, hkl)| { - calculate_in_fibers(lattice_param_a, hkl, s_dir, xtal_orientations, distance_tolerance_rad, in_fibers); + calculate_in_fibers( + &lat_vec_ops_b, + &symm_quat, + hkl, + s_dir, + xtal_orientations, + distance_tolerance_rad, + in_fibers, + ); }); - Ok(()) - } Ok(()) } +/// Lattice strain / light-up calculations for ExaConstit results. See each +/// function's documentation (help(xtal_light_up.xtal_light_up.)) for the +/// expected array shapes. +/// +/// Conventions shared by all of the functions in this module: +/// - Arrays must be C-contiguous numpy float64 arrays (bool for in_fibers); a +/// non-contiguous array raises an error rather than being silently copied. +/// - Symmetric tensors are stored as 6-vectors in the order +/// [11, 22, 33, 23, 13, 12] (tensor components, not engineering shear strains). +/// - Quaternions are [w, x, y, z] and rotate lattice (crystal) frame vectors into +/// the sample frame, the same convention ExaConstit / ExaCMech use. +#[pymodule] +fn xtal_light_up(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(strain_lattice2sample, m)?)?; + m.add_function(wrap_pyfunction!(calc_taylor_factors, m)?)?; + m.add_function(wrap_pyfunction!(calc_directional_stiffness, m)?)?; + m.add_function(wrap_pyfunction!(calc_lattice_strains, m)?)?; + m.add_function(wrap_pyfunction!(calc_taylor_factors_lattice_fiber, m)?)?; + m.add_function(wrap_pyfunction!( + calc_directional_stiffness_lattice_fiber, + m + )?)?; + m.add_function(wrap_pyfunction!(calc_within_fibers, m)?)?; + Ok(()) +} + +/// Total volume of the in-fiber elements for one HKL / step, and the factor each +/// element's volume weighted contribution gets multiplied by. With per_rank_update +/// that factor is 1 (raw volume weighted sums, normalized by the caller once all the +/// data has been accumulated); otherwise it is 1 / total volume (0 if the fiber is +/// empty) so the result is the volume average. #[inline(always)] -fn calc_volume_terms_fiber(evs_step: &[f64], - in_fiber_hkl_step: &[bool], - per_rank_update: bool - ) -> (f64, f64) { - let total_lat_vol = evs_step.into_iter() - .zip(in_fiber_hkl_step) - .filter(|ev_in_fiber| *ev_in_fiber.1) - .map(|(ev, _)| *ev) - .reduce(|ev_total, ev| { - ev_total + ev - }).unwrap_or(0.0_f64); - - let inv_total_lat_vol = - if per_rank_update { +fn calc_volume_terms_fiber( + evs_step: &[f64], + in_fiber_hkl_step: &[bool], + per_rank_update: bool, +) -> (f64, f64) { + let total_lat_vol = evs_step + .iter() + .zip(in_fiber_hkl_step) + .filter(|ev_in_fiber| *ev_in_fiber.1) + .map(|(ev, _)| *ev) + .reduce(|ev_total, ev| ev_total + ev) + .unwrap_or(0.0_f64); + + let inv_total_lat_vol = if per_rank_update { 1.0_f64 } else { if total_lat_vol > f64::EPSILON { @@ -342,33 +572,41 @@ fn calc_volume_terms_fiber(evs_step: &[f64], (total_lat_vol, inv_total_lat_vol) } +/// In-fiber flags for a single HKL: converts the Miller indices to a crystal frame +/// direction with the reciprocal lattice (c = B^T hkl) and hands off to within_fiber. #[inline(always)] -fn calculate_in_fibers(lparam_a: f64, - hkl: &[f64], - s_dir: &[f64], - quats: &[f64], - distance_tolerance: f64, - in_fiber: &mut [bool] +fn calculate_in_fibers( + lat_vec_ops_b: &[[f64; 3]; 3], + symm_quat: &[[f64; 4]], + hkl: &[f64], + s_dir: &[f64], + quats: &[f64], + distance_tolerance: f64, + in_fiber: &mut [bool], ) { - // Computes reciprocal lattice B but different from HEXRD we return as row matrix as that's the easiest way of doing things - let lat_vec_ops_b = compute_lattice_b_param_cubic(lparam_a); - // compute crystal direction from planeData let c_dir = { let mut tmp_cdir = [0.0_f64; 3]; - mat_t_vec_mult::<3, 3, f64>(&lat_vec_ops_b, &hkl, &mut tmp_cdir); + mat_t_vec_mult::<3, 3, f64>(lat_vec_ops_b, hkl, &mut tmp_cdir); tmp_cdir }; - let symm_quat = symmetric_cubic_quaternions(); - - within_fiber::<24>(&c_dir, s_dir, quats, &symm_quat, distance_tolerance, in_fiber); + within_fiber( + &c_dir, + s_dir, + quats, + symm_quat, + distance_tolerance, + in_fiber, + ); } +/// Rotation matrix for a unit quaternion [w, x, y, z]. Same as ExaConstit's +/// Quat2RMat: it takes lattice (crystal) frame vectors into the sample frame. #[inline(always)] fn quat2rmat(quat: &[f64]) -> [[f64; 3]; 3] { assert!(quat.len() >= 4); - let qbar = quat[0] * quat[0] - (quat[1] * quat[1] + quat[2] * quat[2] + quat[3] * quat[3]); + let qbar = quat[0] * quat[0] - (quat[1] * quat[1] + quat[2] * quat[2] + quat[3] * quat[3]); let mut rmat = [[0.0_f64; 3]; 3]; @@ -387,21 +625,25 @@ fn quat2rmat(quat: &[f64]) -> [[f64; 3]; 3] { rmat } +/// Rotates a lattice frame strain 6-vector [11, 22, 33, 23, 13, 12] into the sample +/// frame in place (R strain R^T, with R from quat2rmat). #[inline(always)] -fn rotate_strain_to_sample(quat: &[f64], strain_vec: &mut[f64]) { +fn rotate_strain_to_sample(quat: &[f64], strain_vec: &mut [f64]) { assert!(quat.len() >= 4); assert!(strain_vec.len() >= 6); let rmat = quat2rmat(quat); - let strain = - { + let strain = { let mut strain = [[0.0_f64; 3]; 3]; strain[0][0] = strain_vec[0]; strain[1][1] = strain_vec[1]; strain[2][2] = strain_vec[2]; - strain[1][2] = strain_vec[3]; strain[2][1] = strain[1][2]; - strain[0][2] = strain_vec[4]; strain[2][0] = strain[0][2]; - strain[0][1] = strain_vec[5]; strain[1][0] = strain[0][1]; + strain[1][2] = strain_vec[3]; + strain[2][1] = strain[1][2]; + strain[0][2] = strain_vec[4]; + strain[2][0] = strain[0][2]; + strain[0][1] = strain_vec[5]; + strain[1][0] = strain[0][1]; strain }; @@ -419,300 +661,137 @@ fn rotate_strain_to_sample(quat: &[f64], strain_vec: &mut[f64]) { } } -#[inline(always)] -fn calc_taylor_factor(gdots: &[f64]) -> (f64, f64) { - assert!(gdots.len() >= 12); - let symm_schmid = calculate_fcc_symm_schmid_tensor(); - let mut plastic_def_rate_vec = [0.0_f64; 6]; - mat_t_vec_mult::<12, 6, f64>(&symm_schmid, gdots, &mut plastic_def_rate_vec); - - let eff_plastic_def_rate = { - let norm_vec = norm::<6, f64>(&plastic_def_rate_vec); - norm_vec * f64::sqrt(2.0_f64 / 3.0_f64) - }; - - if eff_plastic_def_rate <= f64::EPSILON { - return (0.0_f64, 0.0) +/// Makes sure the Taylor factor inputs line up: one equivalent plastic strain rate +/// and num_slip shear rates for every element and step. +fn check_taylor_inputs( + num_slip: usize, + num_gdots: usize, + num_dpeffs: usize, + num_values: usize, +) -> anyhow::Result<()> { + if num_slip == 0 { + anyhow::bail!("shear_rates must have at least one slip system"); } - - let abs_sum_shear_rate = { - let mut sum = 0.0_f64; - for gdot in gdots.iter() { - sum += f64::abs(*gdot); - } - sum - }; - - (abs_sum_shear_rate / eff_plastic_def_rate, eff_plastic_def_rate) -} - -#[inline(always)] -fn calculate_fcc_symm_schmid_tensor() -> [[f64; 6]; 12] { - let two = 2.0_f64; - let three = 3.0_f64; - let sqrt_3i = 1.0_f64 / f64::sqrt(three); - let sqrt_2i = 1.0 / f64::sqrt(two); - - let slip_direction = [ - [sqrt_3i, sqrt_3i, sqrt_3i], - [sqrt_3i, sqrt_3i, sqrt_3i], - [sqrt_3i, sqrt_3i, sqrt_3i], - [-sqrt_3i, sqrt_3i, sqrt_3i], - [-sqrt_3i, sqrt_3i, sqrt_3i], - [-sqrt_3i, sqrt_3i, sqrt_3i], - [-sqrt_3i, -sqrt_3i, sqrt_3i], - [-sqrt_3i, -sqrt_3i, sqrt_3i], - [-sqrt_3i, -sqrt_3i, sqrt_3i], - [sqrt_3i, -sqrt_3i, sqrt_3i], - [sqrt_3i, -sqrt_3i, sqrt_3i], - [sqrt_3i, -sqrt_3i, sqrt_3i], - ]; - - let slip_plane_normal = [ - [0.0_f64, sqrt_2i, -sqrt_2i], - [-sqrt_2i, 0.0_f64, sqrt_2i], - [sqrt_2i, -sqrt_2i, 0.0_f64], - [-sqrt_2i, 0.0_f64, -sqrt_2i], - [0.0_f64, -sqrt_2i, sqrt_2i], - [sqrt_2i, sqrt_2i, 0.0_f64], - [0.0_f64, -sqrt_2i, -sqrt_2i], - [sqrt_2i, 0.0_f64, sqrt_2i], - [-sqrt_2i, sqrt_2i, 0.0_f64], - [sqrt_2i, 0.0_f64, -sqrt_2i], - [0.0_f64, sqrt_2i, sqrt_2i], - [-sqrt_2i, -sqrt_2i, 0.0_f64], - ]; - - let mut symm_schmid = [[0.0_f64; 6]; 12]; - let mut skw_schmid = [[0.0_f64; 3]; 12]; - - calculate_schmid_tensor::<12>( - &slip_plane_normal, - &slip_direction, - &mut symm_schmid, - &mut skw_schmid, - ); - - symm_schmid -} - -fn calculate_schmid_tensor( - slip_plane_normal: &[[f64; 3]], - slip_direction: &[[f64; 3]], - symm_schmid: &mut [[f64; 6]], - skw_schmid: &mut [[f64; 3]], -) -{ - assert!(slip_plane_normal.len() >= NSLIP); - assert!(slip_direction.len() >= NSLIP); - assert!(symm_schmid.len() >= NSLIP); - assert!(skw_schmid.len() >= NSLIP); - - let one_half = 0.5_f64; - let sqrt_2i = 1.0 / f64::sqrt(2.0_f64); - - let mut schmid = [[0.0_f64; 3]; 3]; - - for islip in 0..NSLIP { - outer_prod::<3, 3, f64>( - &slip_direction[islip], - &slip_plane_normal[islip], - &mut schmid, + if num_dpeffs != num_values { + anyhow::bail!( + "eq_pl_strain_rate has {} values but {} were expected (one per element and step)", + num_dpeffs, + num_values ); - - // Replace with an inline set of functions - // skew(schmid)[2, 1] = 1/2 * (schmid[2, 1] - schmid[1, 2]) - skw_schmid[islip][0] = one_half * (schmid[2][1] - schmid[1][2]); - // skew(schmid)[0, 2] = 1/2 * (schmid[0, 2] - schmid[2, 0]) - skw_schmid[islip][1] = one_half * (schmid[0][2] - schmid[2][0]); - // skew(schmid)[1, 0] = 1/2 * (schmid[1, 0] - schmid[0, 1]) - skw_schmid[islip][2] = one_half * (schmid[1][0] - schmid[0][1]); - - // Replace with an inline set of functions - // sym(schmid)[0, 0] = 1/2 * (schmid[0, 0] + schmid[0, 0]) - symm_schmid[islip][0] = schmid[0][0]; - // sym(schmid)[1, 1] = 1/2 * (schmid[1, 1] + schmid[1, 1]) - symm_schmid[islip][1] = schmid[1][1]; - // skew(schmid)[2, 2] = 1/2 * (schmid[2, 2] + schmid[2, 2]) - symm_schmid[islip][2] = schmid[2][2]; - // sym(schmid)[1, 2] = 1/2 * (schmid[1, 2] + schmid[2, 1]) - // For consistent dot products replace with sqrt(2)/2 = 1/sqrt(2) - symm_schmid[islip][3] = sqrt_2i * (schmid[2][1] + schmid[1][2]); - // sym(schmid)[0, 2] = 1/2 * (schmid[0, 2] + schmid[2, 0]) - symm_schmid[islip][4] = sqrt_2i * (schmid[0][2] + schmid[2][0]); - // sym(schmid)[0, 1] = 1/2 * (schmid[0, 1] + schmid[1, 0]) - symm_schmid[islip][5] = sqrt_2i * (schmid[1][0] + schmid[0][1]); } + if num_gdots != num_values * num_slip { + anyhow::bail!( + "shear_rates has {} values but {} were expected ({} slip systems per element and step)", + num_gdots, + num_values * num_slip, + num_slip + ); + } + Ok(()) } -/// Computes reciprocal lattice B but different from HEXRD we return as row matrix as that's the easiest way of doing things -#[inline(always)] -fn compute_lattice_b_param_cubic(lparam_a: f64) -> [[f64; 3]; 3] { - let deg90 = std::f64::consts::PI / 2.0_f64; - let cellparms = [lparam_a, lparam_a, lparam_a, deg90, deg90, deg90]; - - let alfa = cellparms[3]; - let beta = cellparms[4]; - let gamma = cellparms[5]; - - let cosalfar = (f64::cos(beta) * f64::cos(gamma) - f64::cos(alfa)) / (f64::sin(beta) * f64::sin(gamma)); - let sinalfar = f64::sqrt(1.0_f64 - cosalfar * cosalfar); - - let a = [cellparms[0], 0.0_f64, 0.0_f64]; - let b = [cellparms[1] * f64::cos(gamma), cellparms[1] * f64::sin(gamma), 0.0_f64]; - let c = [cellparms[2] * f64::cos(beta), -cellparms[2] * cosalfar * f64::sin(beta), cellparms[2] * sinalfar * f64::sin(beta)]; - - // Cell volume - let inv_vol = { - let v_temp = cross_prod(&b, &c); - 1.0 / dot_prod::<3, f64>(&a, &v_temp) - }; - - // Reciprocal lattice vectors - let cross_prod_inv_v = |vec1: &[f64], vec2: &[f64], inv_vol: f64| -> [f64; 3] - { - let mut tmp = cross_prod(vec1, vec2); - tmp[0] *= inv_vol; - tmp[1] *= inv_vol; - tmp[2] *= inv_vol; - tmp - }; - - let astar = cross_prod_inv_v(&b, &c, inv_vol); - let bstar = cross_prod_inv_v(&c, &a, inv_vol); - let cstar = cross_prod_inv_v(&a, &b, inv_vol); - - // B takes components in the reciprocal lattice to X - [astar, bstar, cstar] -} - +/// Taylor factor from the slip system shear rates and ExaCMech's equivalent plastic +/// strain rate. Mirrors LightUp::CalcLatticeTaylorFactorDpeff in ExaConstit, so it +/// works for any crystal system / number of slip systems. #[inline(always)] -fn symmetric_cubic_quaternions() -> [[f64; 4]; 24] { - let angle_axis_symm = [ - [0.0_f64, 1.0_f64, 0.0_f64, 0.0_f64], // identity - [std::f64::consts::FRAC_PI_2, 1.0_f64, 0.0_f64, 0.0_f64], // fourfold about 1 0 0 (x1) - [std::f64::consts::PI, 1.0_f64, 0.0_f64, 0.0_f64], // - [std::f64::consts::FRAC_PI_2 * 3.0_f64, 1.0_f64, 0.0_f64, 0.0_f64], // - [std::f64::consts::FRAC_PI_2, 0.0_f64, 1.0_f64, 0.0_f64], // fourfold about 0 1 0 (x2) - [std::f64::consts::PI, 0.0_f64, 1.0_f64, 0.0_f64], // - [std::f64::consts::FRAC_PI_2 * 3.0_f64, 0.0_f64, 1.0_f64, 0.0_f64], // - [std::f64::consts::FRAC_PI_2, 0.0_f64, 0.0_f64, 1.0_f64], // fourfold about 0 0 1 (x3) - [std::f64::consts::PI, 0.0_f64, 0.0_f64, 1.0_f64], // - [std::f64::consts::FRAC_PI_2 * 3.0_f64, 0.0_f64, 0.0_f64, 1.0_f64], // - [std::f64::consts::FRAC_PI_3 * 2.0_f64, 1.0_f64, 1.0_f64, 1.0_f64], // threefold about 1 1 1 - [std::f64::consts::FRAC_PI_3 * 4.0_f64, 1.0_f64, 1.0_f64, 1.0_f64], // - [std::f64::consts::FRAC_PI_3 * 2.0_f64, -1.0_f64, 1.0_f64, 1.0_f64], // threefold about -1 1 1 - [std::f64::consts::FRAC_PI_3 * 4.0_f64, -1.0_f64, 1.0_f64, 1.0_f64], // - [std::f64::consts::FRAC_PI_3 * 2.0_f64, -1.0_f64, -1.0_f64, 1.0_f64], // threefold about -1 -1 1 - [std::f64::consts::FRAC_PI_3 * 4.0_f64, -1.0_f64, -1.0_f64, 1.0_f64], // - [std::f64::consts::FRAC_PI_3 * 2.0_f64, 1.0_f64, -1.0_f64, 1.0_f64], // threefold about 1 -1 1 - [std::f64::consts::FRAC_PI_3 * 4.0_f64, 1.0_f64, -1.0_f64, 1.0_f64], // - [std::f64::consts::PI, 1.0_f64, 1.0_f64, 0.0_f64], // twofold about 1 1 0 - [std::f64::consts::PI, -1.0_f64, 1.0_f64, 0.0_f64], // twofold about -1 1 0 - [std::f64::consts::PI, 1.0_f64, 0.0_f64, 1.0_f64], // twofold about 1 0 1 - [std::f64::consts::PI, 0.0_f64, 1.0_f64, 1.0_f64], // twofold about 0 1 1 - [std::f64::consts::PI, -1.0_f64, 0.0_f64, 1.0_f64], // twofold about -1 0 1 - [std::f64::consts::PI, 0.0_f64, -1.0_f64, 1.0_f64], // twofold about 0 -1 1 - ]; - - let inv2 = 1.0_f64 / 2.0_f64; - let mut quat_symm = [[0.0_f64; 4]; 24]; - quat_symm.iter_mut() - .zip(angle_axis_symm) - .for_each(|(quat, ang_axis)| { - let s = f64::sin(inv2 * ang_axis[0]); - quat[0] = f64::cos(inv2 * ang_axis[0]); - let mut inv_norm_axis = 1.0 / norm::<3, f64>(&ang_axis[1..4]); - quat[1] = s * ang_axis[1] * inv_norm_axis; - quat[2] = s * ang_axis[2] * inv_norm_axis; - quat[3] = s * ang_axis[3] * inv_norm_axis; - - inv_norm_axis = 1.0_f64; - if quat[0] < 0.0 { - inv_norm_axis *= -1.0_f64; - } - - quat[0] *= inv_norm_axis; - quat[1] *= inv_norm_axis; - quat[2] *= inv_norm_axis; - quat[3] *= inv_norm_axis; - }); - - quat_symm +fn calc_taylor_factor(gdots: &[f64], dpeff: f64) -> f64 { + let abs_sum_shear_rate: f64 = gdots.iter().map(|gdot| f64::abs(*gdot)).sum(); + if f64::abs(dpeff) <= 1.0e-14 { + 0.0_f64 + } else { + abs_sum_shear_rate / dpeff + } } -/// Returns all that aren't -// #[inline(always)] -// fn find_unique_tolerance(rmat_fr_qsym_c_dir: &[[f64; 3]], tolerance: f64) -> Vec<[f64; 3]> { -// rmat_fr_qsym_c_dir.to_vec() -// } - +/// Core in-fiber test shared with ExaConstit (LightUp::CalculateInFibers). +/// +/// For every orientation (quats holds [w, x, y, z] per element) this finds the +/// largest cosine between the sample direction and the symmetrically equivalent +/// crystal directions R_q R_sym^T c, and flags the element as in fiber when the +/// corresponding angle is <= distance_tolerance (radians). in_fibers has one entry +/// per quaternion. #[inline(always)] -fn within_fiber(c_dir: &[f64], - s_dir: &[f64], - quats: &[f64], - symm_quat: &[[f64; 4]], - distance_tolerance: f64, - in_fibers: &mut [bool]) { - +fn within_fiber( + c_dir: &[f64], + s_dir: &[f64], + quats: &[f64], + symm_quat: &[[f64; 4]], + distance_tolerance: f64, + in_fibers: &mut [bool], +) { assert!(c_dir.len() >= 3); assert!(s_dir.len() >= 3); assert!(quats.len() >= 4); - assert!(symm_quat.len() >= SYM_LEN); + assert!(!symm_quat.is_empty()); assert!(in_fibers.len() == (quats.len() / 4)); let c = { let inv_c_norm = 1.0 / norm::<3, f64>(c_dir); - [c_dir[0] * inv_c_norm, c_dir[1] * inv_c_norm, c_dir[2] * inv_c_norm] + [ + c_dir[0] * inv_c_norm, + c_dir[1] * inv_c_norm, + c_dir[2] * inv_c_norm, + ] }; let s = { let inv_s_norm = 1.0 / norm::<3, f64>(s_dir); - [s_dir[0] * inv_s_norm, s_dir[1] * inv_s_norm, s_dir[2] * inv_s_norm] + [ + s_dir[0] * inv_s_norm, + s_dir[1] * inv_s_norm, + s_dir[2] * inv_s_norm, + ] }; // Could maybe move this over to a vec if we want this to be easily generic over a ton of symmetry conditions... - let mut rmat_fr_qsym_c_dir = [[0.0_f64; 3]; SYM_LEN]; - rmat_fr_qsym_c_dir.iter_mut() - .zip(symm_quat) - .for_each(|(prod, quat)| { - let rmat = quat2rmat(quat); - // Might need to make this the transpose... - mat_t_vec_mult::<3, 3, f64>(&rmat, &c, prod); - }); + let mut rmat_fr_qsym_c_dir = vec![[0.0_f64; 3]; symm_quat.len()]; + rmat_fr_qsym_c_dir + .iter_mut() + .zip(symm_quat) + .for_each(|(prod, quat)| { + let rmat = quat2rmat(quat); + // Might need to make this the transpose... + mat_t_vec_mult::<3, 3, f64>(&rmat, &c, prod); + }); // If we really wanted to we could lower try and calculate the elements // that aren't unique here but that's not worth the effort at all given // how fast things are // let c_syms: Vec<[f64; 3]> = find_unique_tolerance::(&rmat_fr_qsym_c_dir, f64::sqrt(f64::EPSILON)); - let s_rmat_csym_prod: Vec = quats.chunks_exact(4).map(|quat| { - let sine = rmat_fr_qsym_c_dir.iter().map(|c_sym| { - let rmat = quat2rmat(quat); - let mut prod = [0.0_f64; 3]; - // Might need to make this the transpose... - mat_vec_mult::<3, 3, f64>(&rmat, c_sym, &mut prod); - dot_prod::<3, f64>(&s, &prod) + let s_rmat_csym_prod: Vec = quats + .as_chunks::<4>() + .0 + .iter() + .map(|quat| { + let sine = rmat_fr_qsym_c_dir + .iter() + .map(|c_sym| { + let rmat = quat2rmat(quat); + let mut prod = [0.0_f64; 3]; + // Might need to make this the transpose... + mat_vec_mult::<3, 3, f64>(&rmat, c_sym, &mut prod); + dot_prod::<3, f64>(&s, &prod) + }) + .fold(f64::MIN, |a, b| a.max(b)); + sine }) - .fold(std::f64::MIN, |a, b| a.max(b)); - sine - }).collect(); - - in_fibers.iter_mut() - .zip(s_rmat_csym_prod) - .for_each(|(in_fiber, sine)| { - let sine_safe = - { - if f64::abs(sine) > 1.00000001 { - sine.signum() - } - else { - sine - } - }; - - let distance = f64::acos(sine_safe); - *in_fiber = distance <= distance_tolerance; - }) -} \ No newline at end of file + .collect(); + + in_fibers + .iter_mut() + .zip(s_rmat_csym_prod) + .for_each(|(in_fiber, sine)| { + let sine_safe = { + if f64::abs(sine) > 1.00000001 { + sine.signum() + } else { + sine + } + }; + + let distance = f64::acos(sine_safe); + *in_fiber = distance <= distance_tolerance; + }) +} diff --git a/scripts/postprocessing/xtal_light_up/xtal_light_up/__init__.py b/scripts/postprocessing/xtal_light_up/xtal_light_up/__init__.py index 10dc208..51d6a71 100644 --- a/scripts/postprocessing/xtal_light_up/xtal_light_up/__init__.py +++ b/scripts/postprocessing/xtal_light_up/xtal_light_up/__init__.py @@ -1,10 +1,12 @@ -# # import the contents of the Rust library into the Python extension -# from xtal_light_up import * +""" +xtal_light_up: lattice strain / "light-up" post-processing for ExaConstit results. -# # optional: include the documentation from the Rust module -# from xtal_light_up import __doc__ # noqa: F401 +The compiled Rust extension lives in the ``xtal_light_up.xtal_light_up`` +submodule, and is typically imported as:: + import xtal_light_up.xtal_light_up as xlup -# class PythonClass: -# def __init__(self, value: int) -> None: -# self.value = value \ No newline at end of file +Use ``help(xlup)`` or ``help(xlup.)`` for the documentation of each +function, including the expected array shapes, and see README.md in the +xtal_light_up source directory for the ExaConstit outputs these need. +""" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 24e830a..af8cd1a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ set(EXACONSTIT_HEADERS postprocessing/mechanics_lightup.hpp sim_state/simulation_state.hpp solvers/mechanics_solver.hpp + solvers/trust_region_solver.hpp utilities/dynamic_function_loader.hpp utilities/mechanics_kernels.hpp utilities/mechanics_log.hpp @@ -59,6 +60,7 @@ set(EXACONSTIT_SOURCES postprocessing/mechanics_lightup.cpp sim_state/simulation_state.cpp solvers/mechanics_solver.cpp + solvers/trust_region_solver.cpp utilities/mechanics_kernels.cpp utilities/unified_logger.cpp ) diff --git a/src/boundary_conditions/BCData.cpp b/src/boundary_conditions/BCData.cpp index 3714bc1..334e650 100644 --- a/src/boundary_conditions/BCData.cpp +++ b/src/boundary_conditions/BCData.cpp @@ -64,7 +64,7 @@ void BCData::SetScales() { } } -void BCData::GetComponents(int id, mfem::Array& component) { +void BCData::GetComponents(int id, std::array& component) { switch (id) { case 0: component[0] = false; diff --git a/src/boundary_conditions/BCData.hpp b/src/boundary_conditions/BCData.hpp index 075e46b..184cb5e 100644 --- a/src/boundary_conditions/BCData.hpp +++ b/src/boundary_conditions/BCData.hpp @@ -5,6 +5,7 @@ #include "mfem.hpp" #include "mfem/linalg/vector.hpp" +#include #include /** @@ -101,6 +102,6 @@ class BCData { * - id = 6: (true, false, true) * - id = 7: (true, true, true) */ - static void GetComponents(int id, mfem::Array& component); + static void GetComponents(int id, std::array& component); }; #endif diff --git a/src/boundary_conditions/BCManager.cpp b/src/boundary_conditions/BCManager.cpp index 5f0e7db..312a685 100644 --- a/src/boundary_conditions/BCManager.cpp +++ b/src/boundary_conditions/BCManager.cpp @@ -13,14 +13,12 @@ void BCManager::UpdateBCData(std::unordered_map>& ess_bdr["total"] = 0; scale = 0.0; - auto ess_comp = map_ess_comp["total"].find(step)->second; - auto ess_id = map_ess_id["total"].find(step)->second; + const auto& ess_comp = map_ess_comp["total"].find(step)->second; + const auto& ess_id = map_ess_id["total"].find(step)->second; - mfem::Array cmp_row; - cmp_row.SetSize(3); + std::array cmp_row; component["total"] = false; - cmp_row = false; for (size_t i = 0; i < ess_id.size(); ++i) { // set the active boundary attributes @@ -48,19 +46,17 @@ void BCManager::UpdateBCData(mfem::Array& ess_bdr, // The size here is set explicitly component.SetSize(ess_bdr.Size(), 3); - mfem::Array cmp_row; - cmp_row.SetSize(3); + std::array cmp_row; component = false; - cmp_row = false; if (map_ess_vel.find(step) == map_ess_vel.end()) { return; } - auto ess_vel = map_ess_vel.find(step)->second; - auto ess_comp = map_ess_comp["ess_vel"].find(step)->second; - auto ess_id = map_ess_id["ess_vel"].find(step)->second; + const auto& ess_vel = map_ess_vel.find(step)->second; + const auto& ess_comp = map_ess_comp["ess_vel"].find(step)->second; + const auto& ess_id = map_ess_id["ess_vel"].find(step)->second; for (size_t i = 0; i < ess_id.size(); ++i) { // set the active boundary attributes @@ -111,19 +107,17 @@ void BCManager::UpdateBCData(mfem::Array& ess_bdr, // The size here is set explicitly component.SetSize(ess_bdr.Size(), 3); - mfem::Array cmp_row; - cmp_row.SetSize(3); + std::array cmp_row; component = false; - cmp_row = false; if (map_ess_vgrad.find(step) == map_ess_vgrad.end()) { return; } - auto ess_vgrad = map_ess_vgrad.find(step)->second; - auto ess_comp = map_ess_comp["ess_vgrad"].find(step)->second; - auto ess_id = map_ess_id["ess_vgrad"].find(step)->second; + const auto& ess_vgrad = map_ess_vgrad.find(step)->second; + const auto& ess_comp = map_ess_comp["ess_vgrad"].find(step)->second; + const auto& ess_id = map_ess_id["ess_vgrad"].find(step)->second; for (size_t i = 0; i < ess_vgrad.size(); ++i) { data[i] = ess_vgrad.at(i); diff --git a/src/fem_operators/mechanics_integrators.cpp b/src/fem_operators/mechanics_integrators.cpp index 9ade98d..8b84b44 100644 --- a/src/fem_operators/mechanics_integrators.cpp +++ b/src/fem_operators/mechanics_integrators.cpp @@ -3,12 +3,14 @@ #include "fem_operators/mechanics_integrators.hpp" #include "utilities/assembly_ops.hpp" +#include "utilities/mechanics_kernels.hpp" #include "utilities/mechanics_log.hpp" -#include "RAJA/RAJA.hpp" #include "mfem.hpp" #include "mfem/general/forall.hpp" +#include "RAJA/RAJA.hpp" + #include #include // cerr #include // log @@ -667,6 +669,112 @@ void ExaNLFIntegrator::AddMultGradPA(const mfem::Vector& x, mfem::Vector& y) con } // End of if statement } +// ----------------------------------------------------------------------------- +// ExaNLFIntegrator::AddMultTransposeGradPA +// +// Native PA kernel computing y += K^T * x where K = B^T D B is the standard +// (non-BBar) tangent stiffness. Mirrors AddMultGradPA exactly except for the +// contraction order against the assembled 4th-order tensor D. +// +// Algorithm per element, per quadrature point: +// 1. Compute physical velocity gradient from input vector and shape function +// derivatives: +// Gx(i,k) = sum_a Gt(a,i,qpt) * X(a,k,elem) +// This is the same operation as the forward kernel since B is independent +// of the gradient transposition. +// +// 2. Apply the TRANSPOSED D tensor contraction: +// T(l,n) = sum_{i,k} D(i,k,l,n,qpt,elem) * Gx(i,k) +// whereas the forward kernel does +// T(i,k) = sum_{l,n} D(i,k,l,n,qpt,elem) * Gx(l,n) +// The difference is *which pair* of D's indices are summed against Gx. +// For symmetric C, D has major symmetry D(i,k,l,n) = D(l,n,i,k) and the +// two contractions agree; for non-symmetric C they disagree. +// +// 3. Apply test-function gradients (same operation as forward kernel): +// Y(a,n) += sum_l Gt(a,l,qpt) * T(l,n) +// +// All quadrature weights and Jacobian determinants are baked into D from the +// AssembleGradPA step, so this kernel does not need to reapply them. +// ----------------------------------------------------------------------------- +void ExaNLFIntegrator::AddMultTransposeGradPA(const mfem::Vector& x, mfem::Vector& y) const { + CALI_CXX_MARK_SCOPE("enlfi_amTGPA"); + if ((space_dims == 1) || (space_dims == 2)) { + MFEM_ABORT("Dimensions of 1 or 2 not supported."); + } else { + const int dim = 3; + const int DIM3 = 3; + const int DIM6 = 6; + + std::array perm3{{2, 1, 0}}; + std::array perm6{{5, 4, 3, 2, 1, 0}}; + + // D tensor from AssembleGradPA: D(elem, qpt, i, k, l, n) + // The leading dim being elem matches the ordering used in the forward kernel. + RAJA::Layout layout_tensor = RAJA::make_permuted_layout( + {{dim, dim, dim, dim, nqpts, nelems}}, perm6); + RAJA::View> D(pa_dmat.Read(), + layout_tensor); + + // Field variables: input/output E-vectors + RAJA::Layout layout_field = RAJA::make_permuted_layout({{nnodes, dim, nelems}}, + perm3); + RAJA::View> X(x.Read(), layout_field); + RAJA::View> Y(y.ReadWrite(), layout_field); + + // Reference shape function derivatives: Gt(node, dim, qpt) + RAJA::Layout layout_grads = RAJA::make_permuted_layout({{nnodes, dim, nqpts}}, perm3); + RAJA::View> Gt(grad.Read(), + layout_grads); + + const int nqpts_ = nqpts; + const int dim_ = dim; + const int nnodes_ = nnodes; + + mfem::forall(nelems, [=] MFEM_HOST_DEVICE(int i_elems) { + for (int j_qpts = 0; j_qpts < nqpts_; j_qpts++) { + // Step 1: Compute velocity gradient at this quadrature point + // Gx(i, k) = sum_a Gt(a, i, qpt) * X(a, k, elem) + double Gx[3][3]; + for (int ii = 0; ii < dim_; ii++) { + for (int kk = 0; kk < dim_; kk++) { + Gx[ii][kk] = 0.0; + for (int a = 0; a < nnodes_; a++) { + Gx[ii][kk] += Gt(a, ii, j_qpts) * X(a, kk, i_elems); + } + } + } + + // Step 2: Apply TRANSPOSED D contraction + // T(l, n) = sum_{i,k} D(i, k, l, n, qpt, elem) * Gx(i, k) + // Compare to forward kernel: + // T(i, k) = sum_{l,n} D(i, k, l, n, qpt, elem) * Gx(l, n) + double T[3][3]; + for (int ll = 0; ll < dim_; ll++) { + for (int nn = 0; nn < dim_; nn++) { + T[ll][nn] = 0.0; + for (int ii = 0; ii < dim_; ii++) { + for (int kk = 0; kk < dim_; kk++) { + T[ll][nn] += D(i_elems, j_qpts, ii, kk, ll, nn) * Gx[ii][kk]; + } + } + } + } + + // Step 3: Apply test-function gradients (same as forward kernel) + // Y(a, n) += sum_l Gt(a, l, qpt) * T(l, n) + for (int nn = 0; nn < dim_; nn++) { + for (int ll = 0; ll < dim_; ll++) { + for (int a = 0; a < nnodes_; a++) { + Y(a, nn, i_elems) += Gt(a, ll, j_qpts) * T[ll][nn]; + } + } + } + } // End of nqpts + }); // End of nelems + } // End of else (3D path) +} + // This assembles the diagonal of our LHS which can be used as a preconditioner void ExaNLFIntegrator::AssembleGradDiagonalPA(mfem::Vector& diag) const { CALI_CXX_MARK_SCOPE("enlfi_AssembleGradDiagonalPA"); @@ -1134,18 +1242,18 @@ void ICExaNLFIntegrator::AssembleElementVector(const mfem::FiniteElement& el, const mfem::IntegrationPoint& ip = irc->IntPoint(i); Ttr.SetIntPoint(&ip); - // compute Jacobian of the transformation - Jpt = Ttr.InverseJacobian(); // Jrt = dxi / dX - + // Shared B-bar implementation (utilities/mechanics_kernels.hpp): the same element + // averages the PA/EA setup and the material models use. el.CalcDShape(ip, DSh); - Mult(DSh, Jpt, DS); // dN_a(xi) / dX = dN_a(xi)/dxi * dxi/dX - DS *= (Ttr.Weight() * ip.weight); - elem_deriv_shapes_loc += DS; - - eVol += (Ttr.Weight() * ip.weight); + exaconstit::kernel::BBarAccumulateQpt(dof, + Ttr.Jacobian().GetData(), + DSh.GetData(), + ip.weight, + elem_deriv_shapes_loc.GetData(), + eVol); } - elem_deriv_shapes_loc *= (1.0 / eVol); + exaconstit::kernel::BBarFinalize(dof, eVol, elem_deriv_shapes_loc.GetData()); double stress[6]; @@ -1219,18 +1327,18 @@ void ICExaNLFIntegrator::AssembleElementGrad(const mfem::FiniteElement& el, const mfem::IntegrationPoint& ip = irc->IntPoint(i); Ttr.SetIntPoint(&ip); - // compute Jacobian of the transformation - Jrt = Ttr.InverseJacobian(); // Jrt = dxi / dX - + // Shared B-bar implementation (utilities/mechanics_kernels.hpp): the same element + // averages the PA/EA setup and the material models use. el.CalcDShape(ip, DSh); - Mult(DSh, Jrt, DS); // dN_a(xi) / dX = dN_a(xi)/dxi * dxi/dX - DS *= (Ttr.Weight() * ip.weight); - elem_deriv_shapes_loc += DS; - - eVol += (Ttr.Weight() * ip.weight); + exaconstit::kernel::BBarAccumulateQpt(dof, + Ttr.Jacobian().GetData(), + DSh.GetData(), + ip.weight, + elem_deriv_shapes_loc.GetData(), + eVol); } - elem_deriv_shapes_loc *= (1.0 / eVol); + exaconstit::kernel::BBarFinalize(dof, eVol, elem_deriv_shapes_loc.GetData()); for (int i = 0; i < ir->GetNPoints(); i++) { const mfem::IntegrationPoint& ip = ir->IntPoint(i); @@ -1257,6 +1365,67 @@ void ICExaNLFIntegrator::AssembleElementGrad(const mfem::FiniteElement& el, return; } +// ----------------------------------------------------------------------------- +// ICExaNLFIntegrator::AssembleGradPA +// +// Sets up geometric data and ensures element-averaged derivatives are ready. +// The B-bar gradient PA does NOT pre-assemble a D tensor (unlike the base +// class) because the volumetric correction couples element-constant data +// (volume-averaged derivatives N̄) with per-quadrature-point data (C, adj(J)) +// in a way that does not fold cleanly into a single pre-assembled tensor. +// Instead, AddMultGradPA / AddMultTransposeGradPA access C directly from the +// quadrature function and apply the B-bar action on the fly in physical space. +// ----------------------------------------------------------------------------- +void ICExaNLFIntegrator::AssembleGradPA(const mfem::Vector& /* x */, + const mfem::FiniteElementSpace& fes) { + this->AssembleGradPA(fes); +} + +void ICExaNLFIntegrator::AssembleGradPA(const mfem::FiniteElementSpace& fes) { + CALI_CXX_MARK_SCOPE("icenlfi_assembleGradPA"); + + mfem::Mesh* mesh = fes.GetMesh(); + const mfem::FiniteElement& el = *fes.GetFE(0); + space_dims = el.GetDim(); + const mfem::IntegrationRule* ir = &( + mfem::IntRules.Get(el.GetGeomType(), 2 * el.GetOrder() + 1)); + + nqpts = ir->GetNPoints(); + nnodes = el.GetDof(); + nelems = fes.GetNE(); + + if ((space_dims == 1) || (space_dims == 2)) { + MFEM_ABORT("Dimensions of 1 or 2 not supported."); + } + + // Cache geometric factors (Jacobians at quadrature points) + geom = mesh->GetGeometricFactors(*ir, mfem::GeometricFactors::JACOBIANS); + + // Cache reference shape function derivatives + if (grad.Size() != (nqpts * space_dims * nnodes)) { + grad.SetSize(nqpts * space_dims * nnodes, mfem::Device::GetMemoryType()); + { + mfem::DenseMatrix DSh; + const int offset = nnodes * space_dims; + double* qpts_dshape_data = grad.HostReadWrite(); + for (int i = 0; i < nqpts; i++) { + const mfem::IntegrationPoint& ip = ir->IntPoint(i); + DSh.UseExternalData(&qpts_dshape_data[offset * i], nnodes, space_dims); + el.CalcDShape(ip, DSh); + } + } + grad.UseDevice(true); + } + + // Element-averaged derivatives N̄(a, k, elem) are computed by AssemblePA(). + // If they have not been computed yet, force a call now so the gradient PA + // kernels can use them. The AssemblePA path is idempotent and safe to call + // even if it has been called previously (it re-zeroes and recomputes). + if (elem_deriv_shapes.Size() != (nnodes * space_dims * nelems)) { + this->AssemblePA(fes); + } +} + /// Method defining element assembly. /** The result of the element assembly is added and stored in the @a emat Vector. */ @@ -1265,6 +1434,7 @@ void ICExaNLFIntegrator::AssembleGradEA(const mfem::Vector& /*x*/, mfem::Vector& emat) { AssembleEA(fes, emat); } + void ICExaNLFIntegrator::AssembleEA(const mfem::FiniteElementSpace& fes, mfem::Vector& emat) { CALI_CXX_MARK_SCOPE("icenlfi_assembleEA"); const mfem::FiniteElement& el = *fes.GetFE(0); @@ -1895,8 +2065,6 @@ void ICExaNLFIntegrator::AssemblePA(const mfem::FiniteElementSpace& fes) { elem_deriv_shapes.UseDevice(); } - elem_deriv_shapes = 0.0; - // geom->J really isn't going to work for us as of right now. We could just reorder it // to the version that we want it to be in instead... if (jacobian.Size() != (dim * dim * nqpts * nelems)) { @@ -1904,12 +2072,8 @@ void ICExaNLFIntegrator::AssemblePA(const mfem::FiniteElementSpace& fes) { jacobian.UseDevice(true); } - const int DIM2 = 2; - const int DIM3 = 3; const int DIM4 = 4; std::array perm4{{3, 2, 1, 0}}; - std::array perm3{{2, 1, 0}}; - std::array perm2{{1, 0}}; RAJA::Layout layout_jacob = RAJA::make_permuted_layout({{dim, dim, nqpts, nelems}}, perm4); @@ -1921,20 +2085,8 @@ void ICExaNLFIntegrator::AssemblePA(const mfem::FiniteElementSpace& fes) { RAJA::View> geom_j_view( geom->J.Read(), layout_geom); - RAJA::Layout layout_egrads = RAJA::make_permuted_layout({{nnodes, dim, nelems}}, - perm3); - RAJA::View> elem_deriv_shapes_view( - elem_deriv_shapes.ReadWrite(), layout_egrads); - - // Transpose of the local gradient variable - RAJA::Layout layout_grads = RAJA::make_permuted_layout({{nnodes, dim, nqpts}}, perm3); - RAJA::View> Gt(grad.Read(), - layout_grads); - - RAJA::Layout layout_adj = RAJA::make_permuted_layout({{dim, dim}}, perm2); const int nqpts_ = nqpts; const int dim_ = dim; - const int nnodes_ = nnodes; mfem::forall(nelems, [=] MFEM_HOST_DEVICE(int i) { for (int j = 0; j < nqpts_; j++) { @@ -1946,72 +2098,384 @@ void ICExaNLFIntegrator::AssemblePA(const mfem::FiniteElementSpace& fes) { } }); - // This loop we'll want to parallelize the rest are all serial for now. - mfem::forall(nelems, [=] MFEM_HOST_DEVICE(int i_elems) { - double adj[dim_ * dim_]; - double c_detJ; - double volume = 0.0; - // So, we're going to say this view is constant however we're going to mutate the values - // only in that one scoped section for the quadrature points. - RAJA::View> A(&adj[0], - layout_adj); - for (int j_qpts = 0; j_qpts < nqpts_; j_qpts++) { - // If we scope this then we only need to carry half the number of variables around - // with us for the adjugate term. - { - const double J11 = J(0, 0, j_qpts, i_elems); // 0,0 - const double J21 = J(1, 0, j_qpts, i_elems); // 1,0 - const double J31 = J(2, 0, j_qpts, i_elems); // 2,0 - const double J12 = J(0, 1, j_qpts, i_elems); // 0,1 - const double J22 = J(1, 1, j_qpts, i_elems); // 1,1 - const double J32 = J(2, 1, j_qpts, i_elems); // 2,1 - const double J13 = J(0, 2, j_qpts, i_elems); // 0,2 - const double J23 = J(1, 2, j_qpts, i_elems); // 1,2 - const double J33 = J(2, 2, j_qpts, i_elems); // 2,2 - const double detJ = J11 * (J22 * J33 - J32 * J23) - - /* */ J21 * (J12 * J33 - J32 * J13) + - /* */ J31 * (J12 * J23 - J22 * J13); - c_detJ = W[j_qpts]; - volume += c_detJ * detJ; - // adj(J) - adj[0] = (J22 * J33) - (J23 * J32); // 0,0 - adj[1] = (J32 * J13) - (J12 * J33); // 0,1 - adj[2] = (J12 * J23) - (J22 * J13); // 0,2 - adj[3] = (J31 * J23) - (J21 * J33); // 1,0 - adj[4] = (J11 * J33) - (J13 * J31); // 1,1 - adj[5] = (J21 * J13) - (J11 * J23); // 1,2 - adj[6] = (J21 * J32) - (J31 * J22); // 2,0 - adj[7] = (J31 * J12) - (J11 * J32); // 2,1 - adj[8] = (J11 * J22) - (J12 * J21); // 2,2 + // Element-averaged shape gradients with the shared B-bar implementation + // (utilities/mechanics_kernels.hpp), the same code the material models use. + exaconstit::kernel::ElementAveragedShapeGradients( + nqpts, nelems, nnodes, jacobian.Read(), grad.Read(), W, elem_deriv_shapes.Write()); + + } // End of space dims if else +} + +// ----------------------------------------------------------------------------- +// ICExaNLFIntegrator::AddMultGradPA +// +// Native B-bar tangent stiffness PA action: y += K̄ * x where +// K̄ = ∫ B̄^T C B̄ dΩ +// and B̄ is the B-bar strain-displacement matrix from Hughes (1980). +// +// Because B̄ couples element-constant volume-averaged data with per-qpt data, +// we work in physical space and access C directly from the simulation state's +// tangent stiffness quadrature function. +// +// Algorithm per element, per quadrature point (q): +// 1. Hoist tr_bar (element-constant) outside the qpt loop: +// tr_bar = sum_{a,k} N̄(a,k) * V(a,k) +// This is the volume-averaged trace of the velocity gradient that B̄ +// uses in place of the per-qpt trace. +// +// 2. Compute the adjugate matrix and Jacobian determinant from the cached +// Jacobian. Adjugate is used to transform reference derivatives Gt to +// physical derivatives: +// dN(a,j) = (1/detJ) * sum_k Gt(a,k,q) * adj(j,k) +// (Adjugate uses inverse-transpose convention; same as in the standard +// ExaNLFIntegrator AssembleGradPA kernel.) +// +// 3. Compute physical velocity gradient: +// L(i,j) = sum_a dN(a,j) * V(a,i) +// +// 4. Compute B-bar trace correction: +// Δtr = (tr_bar - tr(L)) / 3 +// and modified velocity gradient: +// L̄(i,j) = L(i,j) + δ_ij * Δtr +// which replaces the volumetric trace of L with tr_bar (Hughes' B-bar). +// +// 5. Apply material tangent (forward direction): +// σ'(j,k) = sum_{l,m} C(j,k,l,m) * L̄(l,m) +// C is fetched on the fly from the tangent_stiffness quadrature function. +// +// 6. Compute pressure (volumetric) part of σ': +// p' = (1/3) * tr(σ') +// +// 7. Accumulate into Y with B-bar test side. The test side replaces the +// pressure contribution to nodal forces using the volume-averaged +// derivatives N̄ in place of the per-qpt dN: +// Y(a,k) += [sum_j dN(a,j) σ'(j,k) + (N̄(a,k) - dN(a,k)) p'] * w * detJ +// The first term is the standard B^T σ' force, the second redirects the +// pressure piece through N̄. +// +// Verification properties: +// - For symmetric C, the result must equal the forward action of any +// symmetric formulation (B̄^T C B̄ is symmetric). +// - For a uniform-Jacobian mesh where tr_bar agrees with the per-qpt +// trace, Δtr → 0 at every qpt and the result must match the standard +// (non-B-bar) result. +// ----------------------------------------------------------------------------- +void ICExaNLFIntegrator::AddMultGradPA(const mfem::Vector& x, mfem::Vector& y) const { + CALI_CXX_MARK_SCOPE("icenlfi_amGPA"); + if ((space_dims == 1) || (space_dims == 2)) { + MFEM_ABORT("Dimensions of 1 or 2 not supported."); + } else { + const int dim = 3; + const int DIM3 = 3; + const int DIM4 = 4; + const int DIM6 = 6; + + std::array perm3{{2, 1, 0}}; + std::array perm4{{3, 2, 1, 0}}; + std::array perm6{{5, 4, 3, 2, 1, 0}}; + + // Input / output E-vectors + RAJA::Layout layout_field = RAJA::make_permuted_layout({{nnodes, dim, nelems}}, + perm3); + RAJA::View> X(x.Read(), layout_field); + RAJA::View> Y(y.ReadWrite(), layout_field); + + // Reference shape function derivatives Gt(node, dim, qpt) + RAJA::Layout layout_grads = RAJA::make_permuted_layout({{nnodes, dim, nqpts}}, perm3); + RAJA::View> Gt(grad.Read(), + layout_grads); + + // Element-averaged derivatives N̄(node, dim, elem) + RAJA::View> Nbar( + elem_deriv_shapes.Read(), layout_field); + + // Mesh Jacobians J(dim, dim, qpt, elem) — column-major mfem convention + RAJA::Layout layout_jac = RAJA::make_permuted_layout({{dim, dim, nqpts, nelems}}, + perm4); + RAJA::View> J_data(geom->J.Read(), + layout_jac); + + // Material tangent C(j, k, l, m, qpt, elem) from quadrature function + auto tangent_qf = m_sim_state->GetQuadratureFunction("tangent_stiffness"); + RAJA::Layout layout_C = RAJA::make_permuted_layout( + {{dim, dim, dim, dim, nqpts, nelems}}, perm6); + RAJA::View> C(tangent_qf->Read(), + layout_C); + + // Integration weights from the tangent stiffness QF integration rule + const mfem::IntegrationRule& ir = tangent_qf->GetSpaceShared()->GetIntRule(0); + auto W = ir.GetWeights().Read(); + + const int nqpts_ = nqpts; + const int dim_ = dim; + const int nnodes_ = nnodes; + + mfem::forall(nelems, [=] MFEM_HOST_DEVICE(int e) { + // Step 1: Hoist tr_bar outside the qpt loop (element-constant) + double tr_bar = 0.0; + for (int a = 0; a < nnodes_; a++) { + for (int k = 0; k < dim_; k++) { + tr_bar += Nbar(a, k, e) * X(a, k, e); } - for (int knds = 0; knds < nnodes_; knds++) { - elem_deriv_shapes_view(knds, 0, i_elems) += c_detJ * - (Gt(knds, 0, j_qpts) * A(0, 0) + - Gt(knds, 1, j_qpts) * A(0, 1) + - Gt(knds, 2, j_qpts) * A(0, 2)); - - elem_deriv_shapes_view(knds, 1, i_elems) += c_detJ * - (Gt(knds, 0, j_qpts) * A(1, 0) + - Gt(knds, 1, j_qpts) * A(1, 1) + - Gt(knds, 2, j_qpts) * A(1, 2)); - - elem_deriv_shapes_view(knds, 2, i_elems) += c_detJ * - (Gt(knds, 0, j_qpts) * A(2, 0) + - Gt(knds, 1, j_qpts) * A(2, 1) + - Gt(knds, 2, j_qpts) * A(2, 2)); - } // End of nnodes - } // End of nqpts + } + + for (int q = 0; q < nqpts_; q++) { + // Step 2: Compute adjugate and Jacobian determinant + const double J11 = J_data(0, 0, q, e), J12 = J_data(1, 0, q, e), + J13 = J_data(2, 0, q, e); + const double J21 = J_data(0, 1, q, e), J22 = J_data(1, 1, q, e), + J23 = J_data(2, 1, q, e); + const double J31 = J_data(0, 2, q, e), J32 = J_data(1, 2, q, e), + J33 = J_data(2, 2, q, e); + + double adj[9]; + adj[0] = (J22 * J33) - (J23 * J32); // 0,0 + adj[1] = (J23 * J31) - (J21 * J33); // 0,1 + adj[2] = (J21 * J32) - (J22 * J31); // 0,2 + adj[3] = (J13 * J32) - (J12 * J33); // 1,0 + adj[4] = (J11 * J33) - (J13 * J31); // 1,1 + adj[5] = (J12 * J31) - (J11 * J32); // 1,2 + adj[6] = (J12 * J23) - (J13 * J22); // 2,0 + adj[7] = (J13 * J21) - (J11 * J23); // 2,1 + adj[8] = (J11 * J22) - (J12 * J21); // 2,2 + + const double detJ = J11 * adj[0] + J21 * adj[3] + J31 * adj[6]; + const double idetJ = 1.0 / detJ; + const double w_detJ = W[q] * detJ; + + // Step 3: Physical velocity gradient L(i,j) = sum_a dN(a,j) * V(a,i) + // We compute dN(a, :) on-the-fly from Gt and adj. + double L[3][3] = {{0.0}}; + for (int a = 0; a < nnodes_; a++) { + double dNa[3]; + for (int j = 0; j < dim_; j++) { + dNa[j] = idetJ * + (Gt(a, 0, q) * adj[j * 3 + 0] + Gt(a, 1, q) * adj[j * 3 + 1] + + Gt(a, 2, q) * adj[j * 3 + 2]); + } + for (int i = 0; i < dim_; i++) { + for (int j = 0; j < dim_; j++) { + L[i][j] += dNa[j] * X(a, i, e); + } + } + } + + // Step 4: B-bar trace correction + const double tr_std = L[0][0] + L[1][1] + L[2][2]; + const double dtr = (tr_bar - tr_std) / 3.0; + + double Lbar[3][3]; + for (int i = 0; i < dim_; i++) { + for (int j = 0; j < dim_; j++) { + Lbar[i][j] = L[i][j]; + } + } + Lbar[0][0] += dtr; + Lbar[1][1] += dtr; + Lbar[2][2] += dtr; + + // Step 5: Apply material tangent — forward contraction + // σ'(j, k) = sum_{l,m} C(j, k, l, m) * L̄(l, m) + double sigma[3][3] = {{0.0}}; + for (int j = 0; j < dim_; j++) { + for (int k = 0; k < dim_; k++) { + for (int l = 0; l < dim_; l++) { + for (int m = 0; m < dim_; m++) { + sigma[j][k] += C(j, k, l, m, q, e) * Lbar[l][m]; + } + } + } + } + + // Step 6: Pressure (volumetric) part of σ' + const double p = (sigma[0][0] + sigma[1][1] + sigma[2][2]) / 3.0; + + // Step 7: Accumulate forces with B-bar test side + // Y(a, k) += [sum_j dN(a,j) σ'(j,k) + (N̄(a,k) - dN(a,k)) p] * w * detJ + for (int a = 0; a < nnodes_; a++) { + double dNa[3]; + for (int j = 0; j < dim_; j++) { + dNa[j] = idetJ * + (Gt(a, 0, q) * adj[j * 3 + 0] + Gt(a, 1, q) * adj[j * 3 + 1] + + Gt(a, 2, q) * adj[j * 3 + 2]); + } + for (int k = 0; k < dim_; k++) { + double f_std = 0.0; + for (int j = 0; j < dim_; j++) { + f_std += dNa[j] * sigma[j][k]; + } + double f_bbar = (Nbar(a, k, e) - dNa[k]) * p; + Y(a, k, e) += (f_std + f_bbar) * w_detJ; + } + } + } // End of qpts + }); // End of nelems + } // End of else (3D path) +} + +// ----------------------------------------------------------------------------- +// ICExaNLFIntegrator::AddMultTransposeGradPA +// +// Native transposed B-bar tangent stiffness PA action: y += K̄^T * x. +// +// This is structurally IDENTICAL to AddMultGradPA except for one line: the +// material tangent contraction uses C(l,m,j,k) instead of C(j,k,l,m). The +// B-bar geometry (N̄, dN, trace correction, pressure redirection) is the +// same on both sides of K̄ = B̄^T C B̄ because: +// (B̄^T C B̄)^T = B̄^T C^T B̄ +// — only the middle factor C transposes; the outer B̄^T and B̄ remain in +// place. +// +// For symmetric C, this kernel produces results identical to AddMultGradPA +// (a useful verification check). For non-symmetric C (crystal plasticity +// with non-associated flow or non-symmetric Schmid coupling) it produces +// genuinely different results, as required for correct trust-region +// Cauchy point computation. +// ----------------------------------------------------------------------------- +void ICExaNLFIntegrator::AddMultTransposeGradPA(const mfem::Vector& x, mfem::Vector& y) const { + CALI_CXX_MARK_SCOPE("icenlfi_amTGPA"); + if ((space_dims == 1) || (space_dims == 2)) { + MFEM_ABORT("Dimensions of 1 or 2 not supported."); + } else { + const int dim = 3; + const int DIM3 = 3; + const int DIM4 = 4; + const int DIM6 = 6; + + std::array perm3{{2, 1, 0}}; + std::array perm4{{3, 2, 1, 0}}; + std::array perm6{{5, 4, 3, 2, 1, 0}}; + + RAJA::Layout layout_field = RAJA::make_permuted_layout({{nnodes, dim, nelems}}, + perm3); + RAJA::View> X(x.Read(), layout_field); + RAJA::View> Y(y.ReadWrite(), layout_field); + + RAJA::Layout layout_grads = RAJA::make_permuted_layout({{nnodes, dim, nqpts}}, perm3); + RAJA::View> Gt(grad.Read(), + layout_grads); + + RAJA::View> Nbar( + elem_deriv_shapes.Read(), layout_field); + + RAJA::Layout layout_jac = RAJA::make_permuted_layout({{dim, dim, nqpts, nelems}}, + perm4); + RAJA::View> J_data(geom->J.Read(), + layout_jac); + + auto tangent_qf = m_sim_state->GetQuadratureFunction("tangent_stiffness"); + RAJA::Layout layout_C = RAJA::make_permuted_layout( + {{dim, dim, dim, dim, nqpts, nelems}}, perm6); + RAJA::View> C(tangent_qf->Read(), + layout_C); - double ivol = 1.0 / volume; + const mfem::IntegrationRule& ir = tangent_qf->GetSpaceShared()->GetIntRule(0); + auto W = ir.GetWeights().Read(); - for (int knds = 0; knds < nnodes_; knds++) { - elem_deriv_shapes_view(knds, 0, i_elems) *= ivol; - elem_deriv_shapes_view(knds, 1, i_elems) *= ivol; - elem_deriv_shapes_view(knds, 2, i_elems) *= ivol; + const int nqpts_ = nqpts; + const int dim_ = dim; + const int nnodes_ = nnodes; + + mfem::forall(nelems, [=] MFEM_HOST_DEVICE(int e) { + // Step 1: Hoist tr_bar (element-constant) + double tr_bar = 0.0; + for (int a = 0; a < nnodes_; a++) { + for (int k = 0; k < dim_; k++) { + tr_bar += Nbar(a, k, e) * X(a, k, e); + } } - }); // End of mfem::MFEM_FORALL - } // End of space dims if else + for (int q = 0; q < nqpts_; q++) { + // Step 2: Adjugate and Jacobian determinant + const double J11 = J_data(0, 0, q, e), J12 = J_data(1, 0, q, e), + J13 = J_data(2, 0, q, e); + const double J21 = J_data(0, 1, q, e), J22 = J_data(1, 1, q, e), + J23 = J_data(2, 1, q, e); + const double J31 = J_data(0, 2, q, e), J32 = J_data(1, 2, q, e), + J33 = J_data(2, 2, q, e); + + double adj[9]; + adj[0] = (J22 * J33) - (J23 * J32); + adj[1] = (J23 * J31) - (J21 * J33); + adj[2] = (J21 * J32) - (J22 * J31); + adj[3] = (J13 * J32) - (J12 * J33); + adj[4] = (J11 * J33) - (J13 * J31); + adj[5] = (J12 * J31) - (J11 * J32); + adj[6] = (J12 * J23) - (J13 * J22); + adj[7] = (J13 * J21) - (J11 * J23); + adj[8] = (J11 * J22) - (J12 * J21); + + const double detJ = J11 * adj[0] + J21 * adj[3] + J31 * adj[6]; + const double idetJ = 1.0 / detJ; + const double w_detJ = W[q] * detJ; + + // Step 3: Physical velocity gradient + double L[3][3] = {{0.0}}; + for (int a = 0; a < nnodes_; a++) { + double dNa[3]; + for (int j = 0; j < dim_; j++) { + dNa[j] = idetJ * + (Gt(a, 0, q) * adj[j * 3 + 0] + Gt(a, 1, q) * adj[j * 3 + 1] + + Gt(a, 2, q) * adj[j * 3 + 2]); + } + for (int i = 0; i < dim_; i++) { + for (int j = 0; j < dim_; j++) { + L[i][j] += dNa[j] * X(a, i, e); + } + } + } + + // Step 4: B-bar trace correction + const double tr_std = L[0][0] + L[1][1] + L[2][2]; + const double dtr = (tr_bar - tr_std) / 3.0; + + double Lbar[3][3]; + for (int i = 0; i < dim_; i++) { + for (int j = 0; j < dim_; j++) { + Lbar[i][j] = L[i][j]; + } + } + Lbar[0][0] += dtr; + Lbar[1][1] += dtr; + Lbar[2][2] += dtr; + + // Step 5: TRANSPOSED material tangent contraction + // σ'(j, k) = sum_{l,m} C(l, m, j, k) * L̄(l, m) + // (Compare to forward: C(j, k, l, m) * L̄(l, m)) + double sigma[3][3] = {{0.0}}; + for (int j = 0; j < dim_; j++) { + for (int k = 0; k < dim_; k++) { + for (int l = 0; l < dim_; l++) { + for (int m = 0; m < dim_; m++) { + sigma[j][k] += C(l, m, j, k, q, e) * Lbar[l][m]; + } + } + } + } + + // Step 6: Pressure + const double p = (sigma[0][0] + sigma[1][1] + sigma[2][2]) / 3.0; + + // Step 7: Accumulate with B-bar test side (same as forward kernel) + for (int a = 0; a < nnodes_; a++) { + double dNa[3]; + for (int j = 0; j < dim_; j++) { + dNa[j] = idetJ * + (Gt(a, 0, q) * adj[j * 3 + 0] + Gt(a, 1, q) * adj[j * 3 + 1] + + Gt(a, 2, q) * adj[j * 3 + 2]); + } + for (int k = 0; k < dim_; k++) { + double f_std = 0.0; + for (int j = 0; j < dim_; j++) { + f_std += dNa[j] * sigma[j][k]; + } + double f_bbar = (Nbar(a, k, e) - dNa[k]) * p; + Y(a, k, e) += (f_std + f_bbar) * w_detJ; + } + } + } // End of qpts + }); // End of nelems + } // End of else (3D path) } // Here we're applying the following action operation using the assembled "D" 2nd order diff --git a/src/fem_operators/mechanics_integrators.hpp b/src/fem_operators/mechanics_integrators.hpp index fb7d4f7..2311dd4 100644 --- a/src/fem_operators/mechanics_integrators.hpp +++ b/src/fem_operators/mechanics_integrators.hpp @@ -351,6 +351,35 @@ class ExaNLFIntegrator : public mfem::NonlinearFormIntegrator { */ virtual void AddMultGradPA(const mfem::Vector& x, mfem::Vector& y) const override; + /** + * @brief Apply transposed gradient action via partial assembly. + * + * @param x Input vector for transposed Jacobian-vector product + * @param y Output vector for accumulated result + * + * Native PA kernel computing y += K^T * x where K = B^T D B is the + * tangent stiffness operator. The only computational difference from + * AddMultGradPA is the contraction order with the assembled 4th-order + * tensor D: + * + * Forward (AddMultGradPA): + * T(i,k) = D(i,k,l,n,qpt,elem) * Gx(l,n) — contract last pair + * Y(a,k) += Gt(a,i,qpt) * T(i,k) + * + * Transpose (this method): + * T(l,n) = D(i,k,l,n,qpt,elem) * Gx(i,k) — contract first pair + * Y(a,n) += Gt(a,l,qpt) * T(l,n) + * + * For symmetric material tangent C, the two operations are identical. + * For non-symmetric C (crystal plasticity), they differ. The transpose + * is required for trust-region dogleg solver Cauchy point computation + * where the merit function gradient is g = J^T * r, not J * r. + * + * @note GPU-compatible via mfem::forall + * @note Requires prior AssembleGradPA() call for the D tensor + */ + virtual void AddMultTransposeGradPA(const mfem::Vector& x, mfem::Vector& y) const override; + using mfem::NonlinearFormIntegrator::AssemblePA; /** * @brief Initialize partial assembly data structures for residual operations. @@ -723,10 +752,83 @@ class ICExaNLFIntegrator : public ExaNLFIntegrator { const mfem::Vector& /*elfun*/, mfem::DenseMatrix& elmat) override; - // This method doesn't easily extend to PA formulation, so we're punting on - // it for now. - using ExaNLFIntegrator::AddMultGradPA; - using ExaNLFIntegrator::AssembleGradPA; + /** + * @brief Initialize partial assembly data structures for B-bar gradient operations. + * + * @param fes Finite element space providing mesh and element information + * + * Sets up the geometric data needed by AddMultGradPA() and + * AddMultTransposeGradPA() for the B-bar tangent stiffness operator. + * + * Unlike the base class AssembleGradPA() which pre-assembles a 4th-order + * tensor D, the B-bar version stores only the geometric data (Jacobians, + * reference shape function derivatives, and element-averaged derivatives) + * and applies the material tangent C on-the-fly inside the kernel. This + * is because the B-bar correction couples element-constant data (the + * volume-averaged derivatives) with quadrature-point-local data (C and + * adj(J)) in a way that doesn't fold cleanly into a single pre-assembled + * tensor. + * + * Setup steps: + * 1. Cache space_dims, nqpts, nnodes, nelems from the FES + * 2. Get geometric factors (Jacobians at quadrature points) from the mesh + * 3. Compute and cache reference shape function derivatives Gt(a, k, qpt) + * 4. Ensure element-averaged derivatives N̄(a, k, elem) are available + * (calling AssemblePA() if not yet computed) + * + * @note Must be called before AddMultGradPA() or AddMultTransposeGradPA() + * @note Material tangent C is accessed directly from the simulation state + * quadrature function during the AddMult kernels + */ + virtual void AssembleGradPA(const mfem::FiniteElementSpace& fes) override; + + /// State-ful overload that ignores the state vector @a x. + virtual void AssembleGradPA(const mfem::Vector& x, + const mfem::FiniteElementSpace& fes) override; + + /** + * @brief Apply partial-assembly B-bar tangent stiffness action. + * + * @param x Input E-vector (nodal velocities) + * @param y Output E-vector (accumulated) + * + * Computes y += K̄ * x where K̄ = ∫ B̄^T C B̄ dΩ is the B-bar tangent. + * + * Algorithm per element, per quadrature point: + * 1. Compute adj(J) and detJ from the cached Jacobian + * 2. Compute physical derivatives dN(a,j) on-the-fly from Gt and adj(J) + * 3. Compute physical velocity gradient L(i,j) = dN(a,j) V(a,i) + * 4. Compute B-bar trace correction Δtr = (tr_bar - tr(L)) / 3 + * where tr_bar = N̄(a,k) V(a,k) is element-constant (hoisted) + * 5. Modified velocity gradient L̄ = L + δ_ij * Δtr + * 6. Apply C: σ'(j,k) = C(j,k,l,m) * L̄(l,m) + * 7. Pressure correction p' = (1/3) tr(σ') + * 8. Accumulate into Y: standard force + B-bar pressure redirection + * Y(a,k) += [Σ_j dN(a,j) σ'(j,k) + (N̄(a,k) - dN(a,k)) p'] * w * detJ + * + * @note GPU-compatible via mfem::forall + * @note Requires prior AssembleGradPA() call + */ + virtual void AddMultGradPA(const mfem::Vector& x, mfem::Vector& y) const override; + + /** + * @brief Apply transposed B-bar tangent stiffness action. + * + * @param x Input E-vector + * @param y Output E-vector (accumulated) + * + * Computes y += K̄^T * x. Identical to AddMultGradPA except the C + * contraction order is swapped: + * Forward: σ'(j,k) = C(j,k,l,m) * L̄(l,m) + * Transpose: σ'(j,k) = C(l,m,j,k) * L̄(l,m) + * + * The B-bar geometry (N̄, dN, trace correction, pressure redirection) + * is identical for both directions because B̄ appears on both the + * trial and test sides of K̄ = B̄^T C B̄, and (B̄^T C B̄)^T = B̄^T C^T B̄. + * + * @note For symmetric C, this produces identical results to AddMultGradPA + */ + virtual void AddMultTransposeGradPA(const mfem::Vector& x, mfem::Vector& y) const override; /** * @brief Initialize partial assembly data structures for B-bar residual operations. diff --git a/src/fem_operators/mechanics_operator.cpp b/src/fem_operators/mechanics_operator.cpp index b95cd74..a4ef2e4 100644 --- a/src/fem_operators/mechanics_operator.cpp +++ b/src/fem_operators/mechanics_operator.cpp @@ -6,13 +6,23 @@ #include "utilities/mechanics_log.hpp" #include "utilities/unified_logger.hpp" -#include "RAJA/RAJA.hpp" #include "mfem/general/forall.hpp" +#include "RAJA/RAJA.hpp" + #include #include #include +namespace { +void GetTrueDofsParallel(const mfem::ParGridFunction& gf, mfem::Vector& true_dofs) { + // used to do something like: + // gf.GetTrueDofs(true_dofs); + // but looks like there are issues with that on the GPUs with newer versions of MFEM + gf.ParallelAverage(true_dofs); +} +} // namespace + NonlinearMechOperator::NonlinearMechOperator(mfem::Array& ess_bdr, mfem::Array2D& ess_bdr_comp, std::shared_ptr sim_state) @@ -259,7 +269,7 @@ void NonlinearMechOperator::CalculateDeformationGradient(mfem::QuadratureFunctio mfem::Vector x_true(fe_space->TrueVSize(), mfem::Device::GetMemoryType()); - x_cur->GetTrueDofs(x_true); + GetTrueDofsParallel(*x_cur, x_true); // Takes in k vector and transforms into into our E-vector array P->Mult(x_true, px); elem_restrict_lex->Mult(px, el_x); diff --git a/src/fem_operators/mechanics_operator_ext.cpp b/src/fem_operators/mechanics_operator_ext.cpp index 3423beb..243fc21 100644 --- a/src/fem_operators/mechanics_operator_ext.cpp +++ b/src/fem_operators/mechanics_operator_ext.cpp @@ -5,10 +5,11 @@ #include "fem_operators/mechanics_operator.hpp" #include "utilities/mechanics_log.hpp" -#include "RAJA/RAJA.hpp" #include "mfem.hpp" #include "mfem/general/forall.hpp" +#include "RAJA/RAJA.hpp" + MechOperatorJacobiSmoother::MechOperatorJacobiSmoother(const mfem::Vector& d, const mfem::Array& ess_tdofs, const double dmpng) diff --git a/src/mechanics_driver.cpp b/src/mechanics_driver.cpp index 0e9520e..4f3efca 100644 --- a/src/mechanics_driver.cpp +++ b/src/mechanics_driver.cpp @@ -211,7 +211,17 @@ int main(int argc, char* argv[]) { */ mfem::Device device; if (toml_opt.solvers.rtmodel == RTModel::GPU) { +#if defined(MFEM_USE_UMPIRE) + device.SetMemoryTypes(mfem::MemoryType::HOST_UMPIRE, mfem::MemoryType::DEVICE_UMPIRE); +#else device.SetMemoryTypes(mfem::MemoryType::HOST_64, mfem::MemoryType::DEVICE); +#endif + } else { +#if defined(MFEM_USE_UMPIRE) + device.SetMemoryTypes(mfem::MemoryType::HOST_UMPIRE, mfem::MemoryType::DEVICE_UMPIRE); +#else + device.SetMemoryTypes(mfem::MemoryType::HOST_64, mfem::MemoryType::DEVICE); +#endif } device.Configure(device_config.c_str()); diff --git a/src/mfem_expt/partial_qspace.cpp b/src/mfem_expt/partial_qspace.cpp index 2e0261f..3230313 100644 --- a/src/mfem_expt/partial_qspace.cpp +++ b/src/mfem_expt/partial_qspace.cpp @@ -43,6 +43,8 @@ const mfem::Vector& PartialQuadratureSpace::GetGeometricFactorWeights() const { void PartialQuadratureSpace::ConstructOffsets() { // Set up offsets based on our partial element set const int num_partial_elem = local2global.Size(); + ne = num_partial_elem; + full_offset_cache.SetSize(0); offsets.SetSize(num_partial_elem + 1); int offset = 0; for (int i = 0; i < num_partial_elem; i++) { diff --git a/src/models/mechanics_ecmech.cpp b/src/models/mechanics_ecmech.cpp index 23b683d..3c5a496 100644 --- a/src/models/mechanics_ecmech.cpp +++ b/src/models/mechanics_ecmech.cpp @@ -5,11 +5,12 @@ #include "utilities/mechanics_log.hpp" #include "utilities/unified_logger.hpp" +#include "mfem.hpp" +#include "mfem/general/forall.hpp" + #include "ECMech_cases.h" #include "ECMech_const.h" #include "RAJA/RAJA.hpp" -#include "mfem.hpp" -#include "mfem/general/forall.hpp" #include #include // cerr @@ -547,6 +548,44 @@ void ExaCMechModel::ModelSetup(const int nqpts, vel_grad_array_data, local2global_ptr); + // B-bar integrator: the material gets the same volume-averaged kinematics that Bbar + // applies in the residual and tangent (shared implementation in + // utilities/mechanics_kernels.hpp): Lbar = L + (tr_bar - tr L)/3 I in every element, so + // pressure-dependent behavior (porosity, damage) sees an element-constant dilatation + // rate. Only the diagonal of L changes: the deviatoric rate and the spin are untouched. + if (m_sim_state->GetOptions().solvers.integ_model == IntegrationModel::BBAR) { + const auto fe_space = m_sim_state->GetMeshParFiniteElementSpace(); + const mfem::FiniteElement& fe = *fe_space->GetFE(0); + const mfem::IntegrationRule& ir = mfem::IntRules.Get(fe.GetGeomType(), + 2 * fe.GetOrder() + 1); + MFEM_VERIFY(ir.GetNPoints() == nqpts, + "B-bar velocity gradient: the integration rule differs from the model's"); + const int bbar_size = 3 * nnodes * local_nelems; + if (!bbar_shape_grads) { + bbar_shape_grads = std::make_unique(bbar_size, + mfem::Device::GetMemoryType()); + bbar_shape_grads->UseDevice(true); + } else if (bbar_shape_grads->Size() != bbar_size) { + bbar_shape_grads->SetSize(bbar_size, mfem::Device::GetMemoryType()); + } + double* bbar_data = bbar_shape_grads->Write(); + exaconstit::kernel::ElementAveragedShapeGradients(nqpts, + local_nelems, + nnodes, + jacobian_array, + loc_grad_array, + ir.GetWeights().Read(), + bbar_data, + local2global_ptr); + exaconstit::kernel::ApplyBBarVelocityGradient(nqpts, + local_nelems, + nnodes, + bbar_data, + vel_array, + vel_grad_array_data, + local2global_ptr); + } + kernel_setup(npts, nstatev, dt, diff --git a/src/models/mechanics_ecmech.hpp b/src/models/mechanics_ecmech.hpp index 566ff18..0d7111f 100644 --- a/src/models/mechanics_ecmech.hpp +++ b/src/models/mechanics_ecmech.hpp @@ -2,9 +2,10 @@ #include "models/mechanics_model.hpp" +#include "mfem.hpp" + #include "ECMech_const.h" #include "ECMech_matModelBase.h" -#include "mfem.hpp" #include @@ -75,6 +76,16 @@ class ExaCMechModel : public ExaModel { /** @brief Effective deformation rate working array */ std::unique_ptr eff_def_rate; + /** + * @brief Element-averaged physical shape gradients for the B-bar velocity gradient + * + * @details Used only with IntegrationModel::BBAR: nnodes x 3 values per element of this + * region, filled each ModelSetup call by exaconstit::kernel::ElementAveragedShapeGradients + * and consumed by exaconstit::kernel::ApplyBBarVelocityGradient. Allocated on first use + * and resized only if the region's element count changes. + */ + std::unique_ptr bbar_shape_grads; + /** * @brief Mapping from variable names to their locations within the state variable vector * diff --git a/src/models/mechanics_model.cpp b/src/models/mechanics_model.cpp index 1f6e0b2..685e1ac 100644 --- a/src/models/mechanics_model.cpp +++ b/src/models/mechanics_model.cpp @@ -2,10 +2,11 @@ #include "utilities/mechanics_log.hpp" -#include "RAJA/RAJA.hpp" #include "mfem.hpp" #include "mfem/general/forall.hpp" +#include "RAJA/RAJA.hpp" + #include #include // cerr #include // log diff --git a/src/models/mechanics_umat.cpp b/src/models/mechanics_umat.cpp index 069ec6f..c0845a2 100644 --- a/src/models/mechanics_umat.cpp +++ b/src/models/mechanics_umat.cpp @@ -2,12 +2,14 @@ #include "boundary_conditions/BCManager.hpp" #include "utilities/assembly_ops.hpp" +#include "utilities/mechanics_kernels.hpp" #include "utilities/strain_measures.hpp" #include "utilities/unified_logger.hpp" -#include "RAJA/RAJA.hpp" #include "mfem/fem/qfunction.hpp" +#include "RAJA/RAJA.hpp" + #include #include // cerr #include // log @@ -96,6 +98,8 @@ void AbaqusUmatModel::InitLocSFGrads(std::shared_ptr(qspace, VDIM); double* data = loc0_sf_grad->HostReadWrite(); + loc0_wdetj = std::make_shared(qspace, 1); + double* wdetj_data = loc0_wdetj->HostReadWrite(); auto l2g = qspace->GetLocal2Global(); // loop over elements @@ -120,6 +124,7 @@ void AbaqusUmatModel::InitLocSFGrads(std::shared_ptrIntPoint(j); Ttr->SetIntPoint(&ip); + wdetj_data[i * NQPTS + j] = ip.weight * Ttr->Weight(); CalcInverse(Ttr->Jacobian(), Jrt); fe->CalcDShape(ip, DSh); @@ -176,12 +181,11 @@ void AbaqusUmatModel::InitIncrEndDefGrad() { // UPDATED: CalcIncrEndDefGrad now gets defGrad0 from SimulationState void AbaqusUmatModel::CalcIncrEndDefGrad(const mfem::ParGridFunction& x0) { auto loc_fes = m_sim_state->GetMeshParFiniteElementSpace(); + const mfem::IntegrationRule* ir; auto defGrad0 = GetDefGrad0(); auto qspace = defGrad0->GetPartialSpaceShared(); - ir = &(qspace->GetIntRule(0)); - const int nqpts = ir->GetNPoints(); // We've got the same type of elements everywhere so we can do this. // If this assumption is no longer true we need to update the code @@ -193,6 +197,14 @@ void AbaqusUmatModel::CalcIncrEndDefGrad(const mfem::ParGridFunction& x0) { const int vdim2 = loc0_sf_grad->GetVDim(); const int dof = vdim2 / dim; + // B-bar integrator: the UMAT gets the deformation-gradient form of the same volume + // averaging (F-bar, shared implementation in utilities/mechanics_kernels.hpp), so its + // strain increments carry the element-constant volume change V_end / V_beg. + const bool use_fbar = (m_sim_state->GetOptions().solvers.integ_model == IntegrationModel::BBAR); + MFEM_VERIFY(!use_fbar || vdim == dim * dim, + "F-bar: deformation gradients must be stored as 3x3"); + const double* wdetj_data = use_fbar ? loc0_wdetj->HostRead() : nullptr; + double* incr_data = incr_def_grad->HostReadWrite(); double* end_data = end_def_grad->HostReadWrite(); double* int_data = defGrad0->HostReadWrite(); @@ -210,36 +222,35 @@ void AbaqusUmatModel::CalcIncrEndDefGrad(const mfem::ParGridFunction& x0) { mfem::Array vdofs(vdim2); mfem::Vector el_x(PMatI.Data(), vdim2); auto l2g = qspace->GetLocal2Global(); - // loop over elements for (int i = 0; i < ne; ++i) { const int ge = l2g[i]; loc_fes->GetElementVDofs(ge, vdofs); // Our PMatI is now updated to the correct elemental values x_gf.GetSubVector(vdofs, el_x); - // loop over integration points where the quadrature function is - // stored + // End time step def. grad. at every point of the element for (int j = 0; j < nqpts; ++j) { - // The offset is the current location of the data - int offset = (i * nqpts * vdim) + (j * vdim); - int offset2 = (i * nqpts * vdim2) + (j * vdim2); - double* incr_data_offset = incr_data + offset; - double* end_data_offset = end_data + offset; - double* int_data_offset = int_data + offset; - double* ds_data_offset = ds_data + offset2; - - f_end.UseExternalData(end_data_offset, dim, dim); - f_beg.UseExternalData(int_data_offset, dim, dim); - f_incr.UseExternalData(incr_data_offset, dim, dim); - DS.UseExternalData(ds_data_offset, dof, dim); - + const int offset = (i * nqpts * vdim) + (j * vdim); + const int offset2 = (i * nqpts * vdim2) + (j * vdim2); + f_end.UseExternalData(end_data + offset, dim, dim); + DS.UseExternalData(ds_data + offset2, dof, dim); + MultAtB(PMatI, DS, f_end); + } + // F-bar: replace each point's volume ratio by the element's (all points of the + // element are needed first, hence the separate loops) + if (use_fbar) { + exaconstit::kernel::FBarElement( + nqpts, vdim, wdetj_data + i * nqpts, end_data + i * nqpts * vdim); + } + // Incremental def. grad. from the (possibly F-bar) end def. grad. + for (int j = 0; j < nqpts; ++j) { + const int offset = (i * nqpts * vdim) + (j * vdim); + f_end.UseExternalData(end_data + offset, dim, dim); + f_beg.UseExternalData(int_data + offset, dim, dim); + f_incr.UseExternalData(incr_data + offset, dim, dim); // Get the inverse of the beginning time step def. grad f_beg_invr = f_beg; f_beg_invr.Invert(); - - // Find the end time step def. grad - MultAtB(PMatI, DS, f_end); - // Our incremental def. grad is now Mult(f_end, f_beg_invr, f_incr); } diff --git a/src/models/mechanics_umat.hpp b/src/models/mechanics_umat.hpp index dec14bc..a472979 100644 --- a/src/models/mechanics_umat.hpp +++ b/src/models/mechanics_umat.hpp @@ -31,6 +31,15 @@ class AbaqusUmatModel : public ExaModel { /** @brief Initial local shape function gradients working space */ std::shared_ptr loc0_sf_grad; + /** + * @brief Quadrature weight times det(J) in the reference configuration, one value per point + * + * @details Filled with loc0_sf_grad in InitLocSFGrads. With IntegrationModel::BBAR it gives + * the volume weights of the F-bar treatment (exaconstit::kernel::FBarElement), whose + * element-averaged det F is the element's current-to-reference volume ratio. + */ + std::shared_ptr loc0_wdetj; + /** @brief Incremental deformation gradients working space */ std::shared_ptr incr_def_grad; diff --git a/src/options.toml b/src/options.toml index 462dcd3..61a3022 100644 --- a/src/options.toml +++ b/src/options.toml @@ -309,7 +309,8 @@ grain_file = "grains.txt" # - "custom" = user-defined time steps from file (most control) # - "auto" = adaptive time stepping (recommended for efficiency) # - "fixed" = constant time step (simplest) - # Time stepping priority (highest to lowest): + # time_type selects the method even when several sections are given below. + # If time_type is omitted, the priority (highest to lowest) is: # 1. Custom (if [Time.Custom] exists) # 2. Auto (if [Time.Auto] exists) # 3. Fixed (if [Time.Fixed] exists or as default) @@ -457,8 +458,18 @@ grain_file = "grains.txt" # Nonlinear solver type: # - "NR" = standard Newton-Raphson (usually sufficient) # - "NRLS" = Newton with line search (for difficult convergence) + # - "TRDOG" = trust-region dogleg (for difficult convergence) nl_solver = "NR" + # Output verbosity of the nonlinear solver: + # - 0 = only print a one line summary (iterations, final residual, and + # final relative residual) once the solver converges or fails + # - 1 = print the residual and the scale applied to the Newton step + # every iteration (default) + # - 2 = also print detailed information about each trust-region step + # (only used by "TRDOG", same as 1 for the other solvers) + print_info = 1 + # ===================================== # VISUALIZATION OUTPUT # ===================================== @@ -710,8 +721,8 @@ grain_file = "grains.txt" # GEOMETRIC FIELDS # Basic geometric properties computed from finite element mesh # ================================================================= - # "centroid" -> "Centroid" # Element centroid coordinates - # "volume" -> "Volume" # Element volume (or area in 2D) + # "centroid" -> "Element Centroids" # Element centroid coordinates + # "volume" -> "Element Volumes" # Element volume (or area in 2D) # # ================================================================= # STRESS FIELDS diff --git a/src/options/option_boundary_conditions.cpp b/src/options/option_boundary_conditions.cpp index 3502b4f..6ef8ac7 100644 --- a/src/options/option_boundary_conditions.cpp +++ b/src/options/option_boundary_conditions.cpp @@ -1,7 +1,10 @@ #include "options/option_parser_v2.hpp" #include "options/option_util.hpp" +#include +#include #include +#include BCTimeInfo BCTimeInfo::from_toml(const toml::value& toml_input) { BCTimeInfo info; @@ -145,6 +148,69 @@ bool BoundaryOptions::validate() { } } + // Make sure the velocity and velocity gradient BCs aren't constraining the same DOFs + // on the same boundary attribute. The essential_comps values map to the constrained + // {x, y, z} directions as follows (same convention as BCData::GetComponents): + // 0 = none, 1 = x, 2 = y, 3 = z, 4 = xy, 5 = yz, 6 = xz, 7 = xyz + auto constrained_directions = [](int comp) -> std::array { + switch (std::abs(comp)) { + case 1: + return {true, false, false}; + case 2: + return {false, true, false}; + case 3: + return {false, false, true}; + case 4: + return {true, true, false}; + case 5: + return {false, true, true}; + case 6: + return {true, false, true}; + case 7: + return {true, true, true}; + default: + return {false, false, false}; + } + }; + const std::array direction_names = {"x", "y", "z"}; + + bool overlap_found = false; + for (const auto& [step, vel_ids] : map_ess_id["ess_vel"]) { + const auto& vel_comps = map_ess_comp["ess_vel"][step]; + const auto& vgrad_ids = map_ess_id["ess_vgrad"][step]; + const auto& vgrad_comps = map_ess_comp["ess_vgrad"][step]; + for (size_t i = 0; i < vel_ids.size() && i < vel_comps.size(); ++i) { + for (size_t j = 0; j < vgrad_ids.size() && j < vgrad_comps.size(); ++j) { + // Only BCs applied to the same boundary attribute can conflict + if (vel_ids[i] != vgrad_ids[j]) { + continue; + } + const auto vel_dirs = constrained_directions(vel_comps[i]); + const auto vgrad_dirs = constrained_directions(vgrad_comps[j]); + std::string shared_dirs; + for (size_t d = 0; d < 3; ++d) { + if (vel_dirs[d] && vgrad_dirs[d]) { + shared_dirs += direction_names[d] + " "; + } + } + if (!shared_dirs.empty()) { + std::ostringstream err; + err << "Error: `BCs` for update step " << step << " has boundary attribute " + << vel_ids[i] << " with the " << shared_dirs + << "component(s) constrained by both `velocity_bcs` (essential_comps = " + << vel_comps[i] + << ") and `velocity_gradient_bcs` (essential_comps = " << vgrad_comps[j] + << "). A DOF may only be constrained by one BC type."; + WARNING_0_OPT(err.str()); + overlap_found = true; + } + } + } + } + if (overlap_found) { + return false; + } + if (time_info.cycles[0] != 1) { WARNING_0_OPT("Error: `BCs.time_info` needs to have the first value be 1"); return false; diff --git a/src/options/option_enum.cpp b/src/options/option_enum.cpp index 6ae4b99..9f0709e 100644 --- a/src/options/option_enum.cpp +++ b/src/options/option_enum.cpp @@ -106,12 +106,14 @@ LinearSolverType string_to_linear_solver_type(const std::string& str) { /** * @brief Convert string to NonlinearSolverType enum - * @param str String representation of nonlinear solver type ("NR", "NRLS") + * @param str String representation of nonlinear solver type ("NR", "NRLS", "TRDOG") * @return Corresponding NonlinearSolverType enum value */ NonlinearSolverType string_to_nonlinear_solver_type(const std::string& str) { static const std::map mapping = { - {"NR", NonlinearSolverType::NR}, {"NRLS", NonlinearSolverType::NRLS}}; + {"NR", NonlinearSolverType::NR}, + {"NRLS", NonlinearSolverType::NRLS}, + {"TRDOG", NonlinearSolverType::TRDOG}}; return string_to_enum(str, mapping, NonlinearSolverType::NOTYPE, "nonlinear solver"); } diff --git a/src/options/option_material.cpp b/src/options/option_material.cpp index 8db740b..fe13da5 100644 --- a/src/options/option_material.cpp +++ b/src/options/option_material.cpp @@ -19,7 +19,8 @@ GrainInfo GrainInfo::from_toml(const toml::value& toml_input) { info.ori_state_var_loc = toml::find(toml_input, "ori_state_var_loc"); } - if (toml_input.contains("ori_stride")) { + const bool has_ori_stride = toml_input.contains("ori_stride"); + if (has_ori_stride) { info.ori_stride = toml::find(toml_input, "ori_stride"); } @@ -27,6 +28,14 @@ GrainInfo GrainInfo::from_toml(const toml::value& toml_input) { info.ori_type = string_to_ori_type(toml::find(toml_input, "ori_type")); } + if (!has_ori_stride) { + if (info.ori_type == OriType::QUAT) { + info.ori_stride = 4; + } else if (info.ori_type == OriType::EULER) { + info.ori_stride = 3; + } + } + if (toml_input.contains("num_grains")) { info.num_grains = toml::find(toml_input, "num_grains"); } @@ -511,4 +520,4 @@ bool MaterialOptions::validate() const { } return true; -} \ No newline at end of file +} diff --git a/src/options/option_parser_v2.cpp b/src/options/option_parser_v2.cpp index efac46e..7717b20 100644 --- a/src/options/option_parser_v2.cpp +++ b/src/options/option_parser_v2.cpp @@ -2,9 +2,10 @@ #include "options/option_util.hpp" -#include "ECMech_cases.h" #include "mfem.hpp" +#include "ECMech_cases.h" + #include #include #include @@ -16,6 +17,115 @@ namespace fs = std::filesystem; +namespace { + +void apply_exacmech_grain_defaults(MaterialOptions& material) { + if (material.mech_type != MechType::EXACMECH || !material.model.exacmech || + !material.grain_info) { + return; + } + + auto& grain = material.grain_info.value(); + auto index_map = ecmech::modelParamIndexMap(material.model.exacmech->shortcut); + + if (grain.ori_state_var_loc < 0) { + grain.ori_state_var_loc = static_cast(index_map["index_lattice_ori"]); + } + if (grain.ori_stride == 0) { + grain.ori_stride = 4; + } + if (grain.ori_type == OriType::NOTYPE) { + grain.ori_type = OriType::QUAT; + } +} + +void finalize_material_options(ExaOptions& options) { + int max_grains = -1; + int index = 0; + + for (auto& mat : options.materials) { + apply_exacmech_grain_defaults(mat); + + // Grain info (if crystal plasticity) + if (mat.grain_info.has_value()) { + const auto& grain = mat.grain_info.value(); + if (grain.orientation_file.has_value()) { + if (!options.orientation_file.has_value()) { + options.orientation_file = grain.orientation_file.value(); + } + if (grain.orientation_file.value().compare(options.orientation_file.value()) != 0) { + MFEM_ABORT("Check material grain tables as orientation files in there are not " + "consistent between values listed elsewhere"); + } + } + + if (grain.grain_file.has_value()) { + if (!options.grain_file.has_value()) { + options.grain_file = grain.grain_file.value(); + } + if (grain.grain_file.value().compare(options.grain_file.value()) != 0) { + MFEM_ABORT("Check material grain tables as grain files in there are not " + "consistent between values listed elsewhere"); + } + } + + if (max_grains < grain.num_grains && index > 0) { + MFEM_ABORT("Check material grain tables as values in there are not consistent " + "between multiple materials"); + } + + max_grains = grain.num_grains; + index++; + } + } +} + +/** + * @brief Create the targets of any dangling symlinks along a path + * + * @param path Path to check component by component + * @param depth Current recursion depth (guards against symlink cycles) + * @return false if a dangling symlink's target could not be created + * + * @details create_directories() does not create the target of a dangling symlink, it + * just fails since the link itself already exists. A dangling link in the output path + * is a realistic setup though, e.g. `ln -s /remote/storage/location results` done before + * the storage directory itself has been created. So walk the path and create the + * target of every dangling link along it (which may itself contain dangling links). + */ +bool create_dangling_symlink_targets(const fs::path& path, int depth = 0) { + constexpr int max_depth = 40; + if (depth > max_depth) { + return false; + } + fs::path prefix; + for (const auto& part : path) { + prefix /= part; + std::error_code ec; + const auto link_status = fs::symlink_status(prefix, ec); + if (ec || !fs::is_symlink(link_status) || fs::exists(prefix, ec)) { + continue; + } + fs::path target = fs::read_symlink(prefix, ec); + if (ec) { + return false; + } + if (target.is_relative()) { + target = prefix.parent_path() / target; + } + if (!create_dangling_symlink_targets(target, depth + 1)) { + return false; + } + fs::create_directories(target, ec); + if (ec) { + return false; + } + } + return true; +} + +} // namespace + // Implementation of the struct conversion methods void ExaOptions::parse_options(const std::string& filename, int my_id) { try { @@ -49,6 +159,74 @@ void ExaOptions::parse_options(const std::string& filename, int my_id) { mfem::mfem_error(""); } } + + // Resolve the output directory exactly once, on rank 0, and broadcast it so every + // rank and every later consumer (post-processing, logger, visualization) works from + // the same absolute path. Previously each rank resolved symlinks in this path on its + // own, several times over the course of a run, and paired filesystem checks with + // later actions (exists() followed by create_directories(), etc.). With the output + // directory being a symlink to another file system, ranks could see different + // states of that path between those checks and actions (a TOCTOU race). + // + // The directory is created *before* it is resolved: create_directories() follows a + // symlink that is already in place and otherwise creates a real directory, and + // canonical() then returns the physical directory that now exists. Nothing after + // this point goes back through a symlink in the user provided path, so another run + // (e.g. a different ExaConstit run on the same node) creating or changing that + // symlink later on cannot redirect where this run writes. If the path changes in + // the brief window between the two calls, canonical() fails rather than silently + // resolving to somewhere unexpected, and we report it. + // + // All error paths above abort on every rank, so every rank reaches this collective. + // + // If the output directory can't be created or resolved, every rank aborts here rather + // than running the whole simulation only to have none of its output saved. + auto& output_dir = post_processing.volume_averages.output_directory; + std::string resolved_str; + bool output_ok = true; + if (my_id == 0) { + fs::path dir = output_dir.empty() ? fs::path(".") : output_dir; + std::error_code ec; + fs::path abs_dir = fs::absolute(dir, ec); + if (ec) { + abs_dir = dir; + } + fs::path resolved; + if (!create_dangling_symlink_targets(abs_dir)) { + std::cerr << "Error: The output directory " << abs_dir + << " contains a symlink whose target does not exist and could not be " + "created" + << std::endl; + output_ok = false; + } else { + fs::create_directories(abs_dir, ec); + if (!ec) { + resolved = fs::canonical(abs_dir, ec); + } + if (!ec && !fs::is_directory(resolved, ec)) { + ec = std::make_error_code(std::errc::not_a_directory); + } + if (ec || resolved.empty()) { + std::cerr << "Error: Unable to create and resolve the output directory " << abs_dir + << ": " << ec.message() << std::endl; + output_ok = false; + } + } + resolved_str = resolved.string(); + } + MPI_Bcast(&output_ok, 1, MPI_C_BOOL, 0, MPI_COMM_WORLD); + if (!output_ok) { + if (my_id == 0) { + mfem::mfem_error("MFEM_ABORT: Unable to set up the output directory"); + } else { + mfem::mfem_error(""); + } + } + int length = static_cast(resolved_str.size()); + MPI_Bcast(&length, 1, MPI_INT, 0, MPI_COMM_WORLD); + resolved_str.resize(static_cast(length)); + MPI_Bcast(resolved_str.data(), length, MPI_CHAR, 0, MPI_COMM_WORLD); + output_dir = resolved_str; } void ExaOptions::parse_from_toml(const toml::value& toml_input) { @@ -98,6 +276,7 @@ void ExaOptions::parse_from_toml(const toml::value& toml_input) { } else { load_material_files(); } + finalize_material_options(*this); // Parse post-processing from main file if no external file is specified if (!post_processing_file) { @@ -118,38 +297,8 @@ void ExaOptions::parse_time_options(const toml::value& toml_input) { return; } - const auto time_section = toml::find(toml_input, "Time"); - - // Parse restart options - if (time_section.contains("restart")) { - time.restart = toml::find(time_section, "restart"); - } - - if (time_section.contains("restart_time")) { - time.restart_time = toml::find(time_section, "restart_time"); - } - - if (time_section.contains("restart_cycle")) { - time.restart_cycle = toml::find(time_section, "restart_cycle"); - } - - // Parse nested time stepping sections - if (time_section.contains("Auto")) { - time.auto_time = TimeOptions::AutoTimeOptions::from_toml(toml::find(time_section, "Auto")); - } - - if (time_section.contains("Fixed")) { - time.fixed_time = TimeOptions::FixedTimeOptions::from_toml( - toml::find(time_section, "Fixed")); - } - - if (time_section.contains("Custom")) { - time.custom_time = TimeOptions::CustomTimeOptions::from_toml( - toml::find(time_section, "Custom")); - } - - // Determine which time stepping mode to use - time.determine_time_type(); + // Same parsing (restart options, nested sections, time_type) as TimeOptions::from_toml + time = TimeOptions::from_toml(toml::find(toml_input, "Time")); } void ExaOptions::parse_solver_options(const toml::value& toml_input) { @@ -213,42 +362,6 @@ void ExaOptions::parse_material_options(const toml::value& toml_input) { // Add the single material materials.push_back(single_material); } - - int max_grains = -1; - int index = 0; - for (auto& mat : materials) { - // Grain info (if crystal plasticity) - if (mat.grain_info.has_value()) { - const auto& grain = mat.grain_info.value(); - if (grain.orientation_file.has_value()) { - if (!orientation_file.has_value()) { - orientation_file = grain.orientation_file.value(); - } - if (grain.orientation_file.value().compare(orientation_file.value()) != 0) { - MFEM_ABORT("Check material grain tables as orientation files in there are not " - "consistent between values listed elsewhere"); - } - } - - if (grain.grain_file.has_value()) { - if (!grain_file.has_value()) { - grain_file = grain.grain_file.value(); - } - if (grain.grain_file.value().compare(grain_file.value()) != 0) { - MFEM_ABORT("Check material grain tables as grain files in there are not " - "consistent between values listed elsewhere"); - } - } - - if (max_grains < grain.num_grains && index > 0) { - MFEM_ABORT("Check material grain tables as values in there are not consistent " - "between multiple materials"); - } - - max_grains = grain.num_grains; - index++; - } - } } void ExaOptions::parse_model_options(const toml::value& toml_input, MaterialOptions& material) { @@ -652,19 +765,24 @@ void ExaOptions::print_mesh_options() const { void ExaOptions::print_time_options() const { std::cout << "\nTime Stepping Options:\n"; - if (time.time_type == TimeStepType::FIXED) { - std::cout << " Type: Fixed time stepping\n"; + // Which mode was picked and how: an explicit time_type wins over the section priority + const char* selected_by = time.time_type_explicit + ? "set by time_type" + : "selected by section priority Custom > Auto > Fixed"; + + if (time.time_type == TimeStepType::FIXED && time.fixed_time.has_value()) { + std::cout << " Type: Fixed time stepping (" << selected_by << ")\n"; std::cout << " Time step (dt): " << time.fixed_time->dt << "\n"; std::cout << " Final time: " << time.fixed_time->t_final << "\n"; - } else if (time.time_type == TimeStepType::AUTO) { - std::cout << " Type: Automatic time stepping\n"; + } else if (time.time_type == TimeStepType::AUTO && time.auto_time.has_value()) { + std::cout << " Type: Automatic time stepping (" << selected_by << ")\n"; std::cout << " Initial dt: " << time.auto_time->dt_start << "\n"; std::cout << " Minimum dt: " << time.auto_time->dt_min << "\n"; std::cout << " Maximum dt: " << time.auto_time->dt_max << "\n"; std::cout << " Scaling factor: " << time.auto_time->dt_scale << "\n"; std::cout << " Final time: " << time.auto_time->t_final << "\n"; - } else if (time.time_type == TimeStepType::CUSTOM) { - std::cout << " Type: Custom time stepping\n"; + } else if (time.time_type == TimeStepType::CUSTOM && time.custom_time.has_value()) { + std::cout << " Type: Custom time stepping (" << selected_by << ")\n"; std::cout << " Number of steps: " << time.custom_time->nsteps << "\n"; std::cout << " Custom dt file: " << time.custom_time->floc << "\n"; if (!time.custom_time->dt_values.empty()) { @@ -674,6 +792,8 @@ void ExaOptions::print_time_options() const { 0.0) << "\n"; } + } else { + std::cout << " Type: none (time_type has no matching [Time.*] section)\n"; } if (time.restart) { @@ -790,6 +910,9 @@ void ExaOptions::print_solver_options() const { case NonlinearSolverType::NRLS: std::cout << "Newton-Raphson with line search\n"; break; + case NonlinearSolverType::TRDOG: + std::cout << "Trust-region dogleg (SNLS port)\n"; + break; default: std::cout << "Unknown\n"; break; @@ -798,6 +921,43 @@ void ExaOptions::print_solver_options() const { std::cout << " Maximum iterations: " << solvers.nonlinear_solver.iter << "\n"; std::cout << " Relative tolerance: " << solvers.nonlinear_solver.rel_tol << "\n"; std::cout << " Absolute tolerance: " << solvers.nonlinear_solver.abs_tol << "\n"; + std::cout << " Print info level: " << solvers.nonlinear_solver.print_info << "\n"; + + // Trust-region parameters: print if either the solver is TRDOG or the user + // supplied a [trust_region] sub-table. The latter case is informational — + // it lets the user spot misconfigurations where they set TR options without + // selecting the TRDOG solver. + const bool is_trdog = (solvers.nonlinear_solver.nl_solver == NonlinearSolverType::TRDOG); + const bool tr_supplied = solvers.nonlinear_solver.trust_region.has_value(); + + if (is_trdog || tr_supplied) { + std::cout << "\n Trust-region parameters"; + if (is_trdog && !tr_supplied) { + std::cout << " (using defaults)"; + } else if (!is_trdog && tr_supplied) { + std::cout << " (WARNING: supplied but solver is not TRDOG)"; + } + std::cout << ":\n"; + + // Use the supplied options if present, otherwise default-construct + // a TrustRegionOptions to print the defaults + const TrustRegionOptions tr_opts = tr_supplied + ? solvers.nonlinear_solver.trust_region.value() + : TrustRegionOptions{}; + + std::cout << " delta_init = " << tr_opts.delta_init << "\n"; + std::cout << " delta_min = " << tr_opts.delta_min << "\n"; + std::cout << " delta_max = " << tr_opts.delta_max << "\n"; + std::cout << " xi_lg = " << tr_opts.xi_lg << "\n"; + std::cout << " xi_ug = " << tr_opts.xi_ug << "\n"; + std::cout << " xi_lo = " << tr_opts.xi_lo << "\n"; + std::cout << " xi_uo = " << tr_opts.xi_uo << "\n"; + std::cout << " xi_inc = " << tr_opts.xi_inc << "\n"; + std::cout << " xi_dec = " << tr_opts.xi_dec << "\n"; + std::cout << " xi_forced_inc = " << tr_opts.xi_forced_inc << "\n"; + std::cout << " reject_increase = " << (tr_opts.reject_increase ? "true" : "false") + << "\n"; + } } void ExaOptions::print_material_options() const { @@ -1254,4 +1414,4 @@ void ExaOptions::print_post_processing_options() const { } std::cout << "=====================================\n\n"; } -} \ No newline at end of file +} diff --git a/src/options/option_parser_v2.hpp b/src/options/option_parser_v2.hpp index d38fac7..d5494d8 100644 --- a/src/options/option_parser_v2.hpp +++ b/src/options/option_parser_v2.hpp @@ -99,6 +99,7 @@ enum class LinearSolverType { enum class NonlinearSolverType { NR, /**< Newton-Raphson method */ NRLS, /**< Newton-Raphson with line search */ + TRDOG, /**< Trust-region dogleg method (ported from SNLS) */ NOTYPE /**< Uninitialized or invalid nonlinear solver type */ }; @@ -463,6 +464,12 @@ struct TimeOptions { */ TimeStepType time_type = TimeStepType::FIXED; + /** + * @brief Whether time_type came from the time_type option (true) or from the + * Custom > Auto > Fixed section priority (false) + */ + bool time_type_explicit = false; + /** * @brief Auto time stepping options */ @@ -571,9 +578,16 @@ struct TimeOptions { size_t restart_cycle = 0; /** - * @brief Determine which time stepping mode is active based on priority (Custom > Auto > Fixed) + * @brief Determine which time stepping mode is active + * + * @param requested_type Mode given by the time_type option, if any + * + * @details An explicit time_type wins regardless of which sections are present (Fixed + * falls back to default fixed options if [Time.Fixed] is missing). Otherwise, or if the + * time_type string was not recognized, the mode follows the priority Custom > Auto > Fixed + * based on which sections exist. */ - void determine_time_type(); + void determine_time_type(const std::optional& requested_type = std::nullopt); // Static conversion from TOML static TimeOptions from_toml(const toml::value& toml_input); @@ -623,6 +637,103 @@ struct LinearSolverOptions { static LinearSolverOptions from_toml(const toml::value& toml_input); }; +/** + * @brief Trust-region dogleg solver configuration + * + * @details Controls the trust-region radius management and dogleg step + * computation for the ExaTrustRegionSolver. Parameters are ported from + * SNLS's TrDeltaControl with sane defaults suitable for solid mechanics + * applications. Power users can tune these for difficult crystal plasticity + * problems. + * + * The trust-region radius delta is updated based on the ratio + * rho = actual_residual_change / predicted_residual_change + * where predicted change comes from the linearized model at the current iterate. + * + * Acceptance/rejection bands: + * - "Good" band [xi_lg, xi_ug]: increase delta when rho falls here + * - "OK" band [xi_lo, xi_uo]: keep delta when rho falls here (outside good) + * - Outside [xi_lo, xi_uo]: decrease delta + * + * TOML configuration example: + * @code + * [Solvers.NR.trust_region] + * delta_init = 1.0 + * delta_min = 1e-12 + * delta_max = 1e4 + * xi_lg = 0.75 + * xi_ug = 1.4 + * xi_lo = 0.35 + * xi_uo = 5.0 + * xi_inc = 1.5 + * xi_dec = 0.25 + * xi_forced_inc = 1.2 + * reject_increase = true + * @endcode + */ +struct TrustRegionOptions { + /** + * @brief Initial trust-region radius + */ + double delta_init = 1.0; + + /** + * @brief Minimum allowed trust-region radius. Solver fails if delta drops below this. + */ + double delta_min = 1e-12; + + /** + * @brief Maximum allowed trust-region radius + */ + double delta_max = 1e4; + + /** + * @brief Lower bound of the "good" rho band (increase delta when rho > xi_lg) + */ + double xi_lg = 0.75; + + /** + * @brief Upper bound of the "good" rho band + */ + double xi_ug = 1.4; + + /** + * @brief Lower bound of the "ok" rho band (decrease delta when rho < xi_lo) + */ + double xi_lo = 0.35; + + /** + * @brief Upper bound of the "ok" rho band (decrease delta when rho > xi_uo) + */ + double xi_uo = 5.0; + + /** + * @brief Factor used to increase delta when a step is accepted in the "good" band + */ + double xi_inc = 1.5; + + /** + * @brief Factor used to decrease delta when a step quality is outside the "ok" band + */ + double xi_dec = 0.25; + + /** + * @brief Forced-increase factor when the predicted residual change is exactly zero + */ + double xi_forced_inc = 1.2; + + /** + * @brief Whether to reject steps that increase the residual norm + */ + bool reject_increase = true; + + // Validation + bool validate() const; + + // Conversion from toml + static TrustRegionOptions from_toml(const toml::value& toml_input); +}; + /** * @brief Nonlinear solver configuration */ @@ -647,6 +758,25 @@ struct NonlinearSolverOptions { */ NonlinearSolverType nl_solver = NonlinearSolverType::NR; + /** + * @brief Verbosity of the nonlinear solver output + * + * - 0: only a one line summary (number of iterations, final residual, and final + * relative residual) once the solver converges or fails + * - 1: residual and step scale information for every iteration (default) + * - 2+: also prints detailed per-iteration information for solvers that have it + * (currently the trust-region dogleg solver) + */ + int print_info = 1; + + /** + * @brief Trust-region configuration (only used when nl_solver == TRDOG). + * + * If left empty, default TrustRegionOptions values are used. Users with + * difficult convergence problems should provide custom values. + */ + std::optional trust_region; + // Validation bool validate() const; @@ -1235,7 +1365,7 @@ class ExaOptions { /** * @brief Version string for ExaConstit */ - std::string version = "0.8.0"; + std::string version = "0.9.0"; /** * @brief Mesh generation and refinement options diff --git a/src/options/option_solvers.cpp b/src/options/option_solvers.cpp index b5f8af7..a4de829 100644 --- a/src/options/option_solvers.cpp +++ b/src/options/option_solvers.cpp @@ -39,6 +39,63 @@ LinearSolverOptions LinearSolverOptions::from_toml(const toml::value& toml_input return options; } +/** + * @brief Parse trust-region options from a TOML sub-table. + * + * Each field is optional — if not present in the TOML, the struct's default + * value is preserved. This lets users override only the parameters they need + * to tune. + */ +TrustRegionOptions TrustRegionOptions::from_toml(const toml::value& toml_input) { + TrustRegionOptions options; + + if (toml_input.contains("delta_init")) { + options.delta_init = toml::find(toml_input, "delta_init"); + } + + if (toml_input.contains("delta_min")) { + options.delta_min = toml::find(toml_input, "delta_min"); + } + + if (toml_input.contains("delta_max")) { + options.delta_max = toml::find(toml_input, "delta_max"); + } + + if (toml_input.contains("xi_lg")) { + options.xi_lg = toml::find(toml_input, "xi_lg"); + } + + if (toml_input.contains("xi_ug")) { + options.xi_ug = toml::find(toml_input, "xi_ug"); + } + + if (toml_input.contains("xi_lo")) { + options.xi_lo = toml::find(toml_input, "xi_lo"); + } + + if (toml_input.contains("xi_uo")) { + options.xi_uo = toml::find(toml_input, "xi_uo"); + } + + if (toml_input.contains("xi_inc")) { + options.xi_inc = toml::find(toml_input, "xi_inc"); + } + + if (toml_input.contains("xi_dec")) { + options.xi_dec = toml::find(toml_input, "xi_dec"); + } + + if (toml_input.contains("xi_forced_inc")) { + options.xi_forced_inc = toml::find(toml_input, "xi_forced_inc"); + } + + if (toml_input.contains("reject_increase")) { + options.reject_increase = toml::find(toml_input, "reject_increase"); + } + + return options; +} + NonlinearSolverOptions NonlinearSolverOptions::from_toml(const toml::value& toml_input) { NonlinearSolverOptions options; @@ -59,6 +116,18 @@ NonlinearSolverOptions NonlinearSolverOptions::from_toml(const toml::value& toml toml::find(toml_input, "nl_solver")); } + if (toml_input.contains("print_info")) { + options.print_info = toml::find(toml_input, "print_info"); + } + + // Parse the optional trust-region sub-table when using the dogleg solver. + // We always parse the table if present (regardless of nl_solver) so that + // options validation can flag inconsistent configurations later. + if (toml_input.contains("trust_region")) { + options.trust_region = TrustRegionOptions::from_toml( + toml::find(toml_input, "trust_region")); + } + return options; } @@ -123,6 +192,76 @@ bool LinearSolverOptions::validate() const { return true; } +/** + * @brief Validate trust-region option ranges and consistency. + * + * Step-by-step verification: + * 1. Trust-region radius bounds: delta_min must be positive and delta_max + * must exceed delta_min + * 2. Initial radius must lie within [delta_min, delta_max] + * 3. The "good" rho band [xi_lg, xi_ug] must lie inside the "ok" band + * [xi_lo, xi_uo] — otherwise the radius update logic is inconsistent + * 4. Increase factors must be > 1 and decrease factor must be in (0, 1) + * + * Each failure is reported with WARNING_0_OPT pointing to the offending field. + */ +bool TrustRegionOptions::validate() const { + if (delta_min <= 0.0) { + WARNING_0_OPT("Error: TrustRegion table provided a non-positive delta_min"); + return false; + } + + if (delta_max <= delta_min) { + WARNING_0_OPT("Error: TrustRegion table provided delta_max <= delta_min"); + return false; + } + + if (delta_init < delta_min || delta_init > delta_max) { + WARNING_0_OPT( + "Error: TrustRegion table provided delta_init outside [delta_min, delta_max]"); + return false; + } + + if (xi_lg <= xi_lo) { + WARNING_0_OPT("Error: TrustRegion table requires xi_lg > xi_lo " + "(good band must lie inside ok band)"); + return false; + } + + if (xi_ug >= xi_uo) { + WARNING_0_OPT("Error: TrustRegion table requires xi_ug < xi_uo " + "(good band must lie inside ok band)"); + return false; + } + + if (xi_lg >= xi_ug) { + WARNING_0_OPT("Error: TrustRegion table requires xi_lg < xi_ug"); + return false; + } + + if (xi_lo >= xi_uo) { + WARNING_0_OPT("Error: TrustRegion table requires xi_lo < xi_uo"); + return false; + } + + if (xi_inc <= 1.0) { + WARNING_0_OPT("Error: TrustRegion table requires xi_inc > 1.0"); + return false; + } + + if (xi_dec <= 0.0 || xi_dec >= 1.0) { + WARNING_0_OPT("Error: TrustRegion table requires xi_dec in (0, 1)"); + return false; + } + + if (xi_forced_inc <= 1.0) { + WARNING_0_OPT("Error: TrustRegion table requires xi_forced_inc > 1.0"); + return false; + } + + return true; +} + bool NonlinearSolverOptions::validate() const { if (iter < 1) { WARNING_0_OPT("Error: NonLinearSolver table did not provide a positive iteration count"); @@ -139,13 +278,27 @@ bool NonlinearSolverOptions::validate() const { return false; } - if (nl_solver != NonlinearSolverType::NR && nl_solver != NonlinearSolverType::NRLS) { - WARNING_0_OPT("Error: NonLinearSolver table did not provide a valid nl_solver option (`NR` " - "or `NRLS`)"); + if (print_info < 0) { + WARNING_0_OPT("Error: NonLinearSolver table provided a negative print_info value"); return false; } - // Implement validation logic + if (nl_solver != NonlinearSolverType::NR && nl_solver != NonlinearSolverType::NRLS && + nl_solver != NonlinearSolverType::TRDOG) { + WARNING_0_OPT("Error: NonLinearSolver table did not provide a valid nl_solver option " + "(`NR`, `NRLS`, or `TRDOG`)"); + return false; + } + + // If trust-region parameters were supplied, verify they are self-consistent. + // We allow a TRDOG solver without a [trust_region] sub-table — the defaults + // are applied in that case. + if (trust_region.has_value()) { + if (!trust_region->validate()) { + return false; + } + } + return true; } diff --git a/src/options/option_time.cpp b/src/options/option_time.cpp index e8ac856..cf2a760 100644 --- a/src/options/option_time.cpp +++ b/src/options/option_time.cpp @@ -91,7 +91,24 @@ bool TimeOptions::CustomTimeOptions::load_custom_dt_values() { } } -void TimeOptions::determine_time_type() { +void TimeOptions::determine_time_type(const std::optional& requested_type) { + // An explicit time_type takes precedence over which sections are present. An unknown + // string has already been warned about and comes in as NOTYPE: fall back to the priority. + if (requested_type.has_value() && requested_type.value() != TimeStepType::NOTYPE) { + time_type = requested_type.value(); + time_type_explicit = true; + if (time_type == TimeStepType::FIXED && !fixed_time.has_value()) { + fixed_time = FixedTimeOptions{}; + } else if ((time_type == TimeStepType::AUTO && !auto_time.has_value()) || + (time_type == TimeStepType::CUSTOM && !custom_time.has_value())) { + const std::string section = (time_type == TimeStepType::AUTO) ? "Auto" : "Custom"; + WARNING_0_OPT("Error: Time.time_type requests " + section + + " time stepping but no [Time." + section + "] section was provided"); + } + return; + } + + time_type_explicit = false; if (custom_time.has_value()) { time_type = TimeStepType::CUSTOM; } else if (auto_time.has_value()) { @@ -135,7 +152,11 @@ TimeOptions TimeOptions::from_toml(const toml::value& toml_input) { } // Determine which time stepping mode to use - options.determine_time_type(); + std::optional requested_type; + if (toml_input.contains("time_type")) { + requested_type = string_to_time_step_type(toml::find(toml_input, "time_type")); + } + options.determine_time_type(requested_type); return options; } diff --git a/src/postprocessing/mechanics_lightup.cpp b/src/postprocessing/mechanics_lightup.cpp index 7135273..42e3e6d 100644 --- a/src/postprocessing/mechanics_lightup.cpp +++ b/src/postprocessing/mechanics_lightup.cpp @@ -3,10 +3,11 @@ #include "utilities/mechanics_kernels.hpp" #include "utilities/rotations.hpp" +#include "mfem/general/forall.hpp" + #include "ECMech_const.h" #include "ECMech_gpu_portability.h" #include "SNLS_linalg.h" -#include "mfem/general/forall.hpp" /** * @brief Type trait for detecting std::array types @@ -172,7 +173,7 @@ std::vector> GetSymmetryGroups(const LatticeType& lattice_ {FRAC_PI_3 * 4.0, 0.0, 0.0, 1.0}, {PI, 1.0, 0.0, 0.0}, // twofold about 2 -1 -1 0 (x1,a1) {PI, -0.5, SQRT3_2, 0.0}, // twofold about -1 2 -1 0 (a2) - {PI, -0.5, SQRT3_2, 0.0} // twofold about -1 -1 2 0 (a3) + {PI, -0.5, -SQRT3_2, 0.0} // twofold about -1 -1 2 0 (a3) }; break; } diff --git a/src/postprocessing/postprocessing_driver.cpp b/src/postprocessing/postprocessing_driver.cpp index 8212eb2..f709fe4 100644 --- a/src/postprocessing/postprocessing_driver.cpp +++ b/src/postprocessing/postprocessing_driver.cpp @@ -367,7 +367,8 @@ PostProcessingDriver::PostProcessingDriver(std::shared_ptr sim_ : m_sim_state(sim_state), m_mpi_rank(0), m_num_regions(sim_state->GetNumberOfRegions()), m_aggregation_mode(AggregationMode::BOTH), m_enable_visualization(options.visualization.visit || options.visualization.conduit || - options.visualization.paraview || options.visualization.adios2) { + options.visualization.paraview || options.visualization.adios2), + m_viz_output_frequency(options.visualization.output_frequency) { MPI_Comm_rank(MPI_COMM_WORLD, &m_mpi_rank); MPI_Comm_size(MPI_COMM_WORLD, &m_num_mpi_rank); @@ -531,17 +532,18 @@ void PostProcessingDriver::UpdateFields([[maybe_unused]] const int step, void PostProcessingDriver::Update(const int step, const double time) { CALI_CXX_MARK_SCOPE("postprocessing_update"); UpdateFields(step, time); - // Check if we should output volume averages at this step - if (ShouldOutputAtStep(step)) { - PrintVolValues(time, m_aggregation_mode); - ClearVolumeAverageCache(); - } // Update data collections for visualization - if (m_enable_visualization) { + if (ShouldOutputVisualizationAtStep(step) && m_enable_visualization) { UpdateDataCollections(step, time); } + // Check if we should output volume averages at this step + if (ShouldOutputAtStep(step)) { + PrintVolValues(time, m_aggregation_mode); + } + ClearVolumeAverageCache(); + if (m_light_up_instances.size() > 0) { UpdateLightUpAnalysis(); } @@ -1021,6 +1023,10 @@ bool PostProcessingDriver::ShouldOutputAtStep(int step) const { return m_file_manager->ShouldOutputAtStep(step); } +bool PostProcessingDriver::ShouldOutputVisualizationAtStep(int step) const { + return (step % m_viz_output_frequency == 0); +} + void PostProcessingDriver::VolumeAvgStress(const int region, const double time) { VolumeAverage("stress", region, time); } @@ -1280,8 +1286,9 @@ void PostProcessingDriver::CalcElementAvg(mfem::expt::PartialQuadratureFunction* *ir, mfem::GeometricFactors::DETERMINANTS); // KEY DIFFERENCE: Get the local-to-global element mapping for partial space - auto l2g = pqs->GetLocal2Global().Read(); // Maps local element index to global element index - auto loc_offsets = pqs->getOffsets().Read(); // Offsets for local data layout + auto l2g = pqs->GetLocal2Global().Read(); // Maps local element index to global element index + auto loc_offsets = + pqs->Offsets(mfem::QSpaceOffsetStorage::COMPRESSED).Read(); // Offsets for local data layout // auto global_offsets = (pqs->GetGlobalOffset().Size() > 1) ? // pqs->GetGlobalOffset().Read() : loc_offsets; // Offsets for global // data layout @@ -1393,6 +1400,9 @@ void PostProcessingDriver::InitializeGridFunctions() { const auto gf_name = GetGridFunctionName(reg.display_name, reg_int); // Determine vector dimension from quadrature function const int vdim = reg.region_length[region]; + if (vdim < 1) { + continue; + } max_vdim = (vdim > max_vdim) ? vdim : max_vdim; auto fe_space = GetParFiniteElementSpace(reg_int, vdim); m_map_gfs.emplace(gf_name, @@ -1467,18 +1477,31 @@ void PostProcessingDriver::InitializeDataCollections(ExaOptions& options) { return input.substr(0, pos); }; + auto has_registered_fields = [this](const std::string& display_region_postfix) { + for (const auto& [key, value] : m_map_gfs) { + (void)value; + if (key.find(display_region_postfix) != std::string::npos) { + return true; + } + } + return false; + }; + if (m_aggregation_mode == AggregationMode::PER_REGION || m_aggregation_mode == AggregationMode::BOTH) { for (int region = 0; region < static_cast(m_num_regions); ++region) { auto mesh = m_map_submesh[region]; std::string region_postfix = "region_" + std::to_string(region + 1); std::string display_region_postfix = " " + m_sim_state->GetRegionDisplayName(region); + if (!has_registered_fields(display_region_postfix)) { + continue; + } fs::path output_dir = output_dir_base / region_postfix; fs::path output_dir_vizs = output_dir / m_file_manager->GetBaseFilename(); - if (m_sim_state->IsRegionActive(region)) { - auto region_comm = m_sim_state->GetRegionCommunicator(region); - m_file_manager->EnsureDirectoryExists(output_dir, region_comm); - } + // The subsequent DataCollection::Save() is a parallel operation on the submesh's + // communicator, which is still the parent MPI communicator. Prepare directories on + // that same communicator so all participating ranks observe the same path state. + m_file_manager->EnsureDirectoryExists(output_dir, MPI_COMM_WORLD); std::vector dcs_keys; if (options.visualization.visit) { std::string key = visit_key + region_postfix; @@ -1534,6 +1557,9 @@ void PostProcessingDriver::InitializeDataCollections(ExaOptions& options) { std::string region_postfix = "global"; std::string display_region_postfix = " " + m_sim_state->GetRegionDisplayName(-1); + if (!has_registered_fields(display_region_postfix)) { + return; + } fs::path output_dir = output_dir_base / region_postfix; fs::path output_dir_vizs = output_dir / m_file_manager->GetBaseFilename(); m_file_manager->EnsureDirectoryExists(output_dir); diff --git a/src/postprocessing/postprocessing_driver.hpp b/src/postprocessing/postprocessing_driver.hpp index 3ccaa68..a4242c4 100644 --- a/src/postprocessing/postprocessing_driver.hpp +++ b/src/postprocessing/postprocessing_driver.hpp @@ -4,9 +4,10 @@ #include "sim_state/simulation_state.hpp" #include "utilities/mechanics_kernels.hpp" -#include "ECMech_const.h" #include "mfem.hpp" +#include "ECMech_const.h" + // Forward declaration to avoid circular includes class PostProcessingFileManager; @@ -129,6 +130,17 @@ class PostProcessingDriver { */ bool ShouldOutputAtStep(int step) const; + /** + * @brief Check if visualization data collections should be output at current step + * + * @param step Current time step number + * @return true if output should occur, false otherwise + * + * Determines output timing based on the visualization output frequency + * in ExaOptions, which is independent of the volume average output frequency. + */ + bool ShouldOutputVisualizationAtStep(int step) const; + // Returns a pointer to a ParFiniteElementSpace (PFES) that's ordered according to VDIMs // and makes use of an L2 FiniteElementCollection // If the vdim is not in the internal mapping than a new PFES will be created @@ -973,6 +985,14 @@ class PostProcessingDriver { */ bool m_enable_visualization; + /** + * @brief Visualization output frequency (every N time steps) + * + * Taken from the visualization options and kept separate from the + * volume average output frequency used by the file manager. + */ + int m_viz_output_frequency; + /** * @brief Active LightUp analysis instances * diff --git a/src/postprocessing/postprocessing_file_manager.hpp b/src/postprocessing/postprocessing_file_manager.hpp index f070029..bf070de 100644 --- a/src/postprocessing/postprocessing_file_manager.hpp +++ b/src/postprocessing/postprocessing_file_manager.hpp @@ -185,11 +185,13 @@ class PostProcessingFileManager { auto filepath = GetVolumeAverageFilePath(calc_type, region, region_name); - bool file_exists = fs::exists(filepath); - auto file = CreateOutputFile(filepath, true); + auto file = CreateOutputFile(filepath, true, comm); if (file && file->is_open()) { - if (!file_exists) { + // Write the header if the file is empty. Checking the opened file itself + // rather than calling exists() beforehand avoids another check-then-act gap. + file->seekp(0, std::ios_base::end); + if (file->tellp() == 0) { *file << GetVolumeAverageHeader(calc_type); } @@ -376,8 +378,12 @@ inline PostProcessingFileManager::PostProcessingFileManager(const ExaOptions& op if (m_output_directory.empty()) { m_output_directory = "."; } - // Resolve symlinks and build path - m_output_directory = fs::weakly_canonical(m_output_directory) / m_base_filename; + // The output directory has already been resolved to an absolute path once on rank 0 + // and broadcast to all ranks in ExaOptions::parse_options. We purposefully do not + // resolve it again here, as resolving it independently on every rank (and for every + // file manager instance) is what opened the door to ranks seeing different states of + // a symlinked output directory. + m_output_directory = m_output_directory / m_base_filename; if (options.visualization.visit || options.visualization.paraview || options.visualization.adios2) { @@ -453,76 +459,43 @@ inline bool PostProcessingFileManager::EnsureDirectoryExists(fs::path& output_di MPI_Comm_rank(comm, &rank); bool success = false; if (rank == 0) { - try { - // Use weakly_canonical to resolve as much as possible - // This handles symlinks and normalizes the path - - // Example: output_dir = "./results/test_case1/visualizations" - // where ./results is a symlink to /storage/results - // but test_case1/visualizations doesn't exist yet - - // weakly_canonical will resolve to: - // /storage/results/test_case1/visualizations - fs::path canonical_path = fs::weakly_canonical(output_dir); - - // Now check if this canonical path exists - if (fs::exists(canonical_path)) { - if (!fs::is_directory(canonical_path)) { - std::cerr << "Error: Path exists but is not a directory: " << canonical_path - << std::endl; - success = false; - } else { - std::cout << "Using existing directory: " << canonical_path << std::endl; - output_dir = canonical_path; - success = true; - } - } else { - // Directory doesn't exist, create it - std::cout << "Creating output directory: " << canonical_path << std::endl; - success = fs::create_directories(canonical_path); - if (success) { - output_dir = canonical_path; - } else { - std::cerr << "Warning: Failed to create output directory: " << canonical_path - << std::endl; - } - } + // Only rank 0 touches the file system, and it does so without first checking + // whether the directory exists. create_directories() is a no-op for directories + // that already exist, so a separate exists() check would only add a window where + // the path could change between the check and the creation (TOCTOU). + // output_dir is used as given; it is built from the output directory that was + // already resolved once in ExaOptions::parse_options. + std::error_code ec; + const bool created = fs::create_directories(output_dir, ec); + if (ec) { + std::cerr << "Error: Failed to create output directory " << output_dir << ": " + << ec.message() << std::endl; + } else if (!fs::is_directory(output_dir, ec)) { + std::cerr << "Error: Path exists but is not a directory: " << output_dir << std::endl; + } else { + std::cout << (created ? "Creating output directory: " : "Using existing directory: ") + << output_dir << std::endl; + success = true; + } - // Write test remains the same... - if (success) { - fs::path test_file = canonical_path / "test_write.tmp"; - std::ofstream test_stream(test_file); - if (!test_stream.is_open()) { - success = false; - std::cerr << "Warning: Output directory is not writable: " << canonical_path - << std::endl; - } else { - test_stream.close(); - fs::remove(test_file); - } + // Make sure we can actually write to the directory + if (success) { + fs::path test_file = output_dir / "test_write.tmp"; + std::ofstream test_stream(test_file); + if (!test_stream.is_open()) { + success = false; + std::cerr << "Warning: Output directory is not writable: " << output_dir + << std::endl; + } else { + test_stream.close(); + fs::remove(test_file, ec); } - } catch (const fs::filesystem_error& ex) { - success = false; - std::cerr << "Filesystem error when creating directory " << output_dir << ": " - << ex.what() << std::endl; - } catch (const std::exception& ex) { - success = false; - std::cerr << "Error when creating directory " << output_dir << ": " << ex.what() - << std::endl; } } - // Broadcast the potentially updated output_dir to all ranks - std::string path_str = output_dir.string(); - int dir_length = static_cast(path_str.length()); - MPI_Bcast(&dir_length, 1, MPI_INT, 0, comm); - path_str.resize(static_cast(dir_length)); - MPI_Bcast(&path_str[0], dir_length, MPI_CHAR, 0, comm); - output_dir = path_str; - - bool success_t = false; - MPI_Allreduce(&success, &success_t, 1, MPI_C_BOOL, MPI_LOR, comm); - return success_t; + // All ranks wait until rank 0 is done so the directory exists before anyone uses it + MPI_Bcast(&success, 1, MPI_C_BOOL, 0, comm); + return success; } inline bool PostProcessingFileManager::EnsureOutputDirectoryExists() { @@ -540,18 +513,23 @@ PostProcessingFileManager::CreateOutputFile(const fs::path& filepath, bool appen int rank; MPI_Comm_rank(comm, &rank); try { - // Use weakly_canonical to resolve symlinks in the path - fs::path resolved_path = fs::weakly_canonical(filepath); + // Note, this is only called by the rank that writes the file (e.g. rank 0 of a + // region's communicator), so nothing in here may be collective. This previously + // called MPI_Barrier(comm) whenever the directory appeared to be missing, which + // would hang since the other ranks never call this function. + // + // The path is used as given rather than re-resolving symlinks in it, and the + // parent directory is created without a separate exists() check beforehand + // (create_directories() is a no-op if it already exists). + const fs::path& resolved_path = filepath; fs::path dir_path = resolved_path.parent_path(); - - // Ensure directory exists (only check and create if needed) - if (!dir_path.empty() && !fs::exists(dir_path)) { - if (rank == 0) { - std::cout << "Creating directory: " << dir_path << std::endl; - fs::create_directories(dir_path); + if (!dir_path.empty()) { + std::error_code ec; + fs::create_directories(dir_path, ec); + if (ec) { + std::cerr << "Warning: Failed to create directory " << dir_path << ": " + << ec.message() << std::endl; } - // Synchronize to ensure directory is created before all ranks proceed - MPI_Barrier(comm); } // Open file diff --git a/src/sim_state/simulation_state.cpp b/src/sim_state/simulation_state.cpp index 0266248..5ca05b7 100644 --- a/src/sim_state/simulation_state.cpp +++ b/src/sim_state/simulation_state.cpp @@ -287,6 +287,10 @@ TimeStep TimeManagement::UpdateDeltaTime(const int nr_steps, const bool success) if (internal_tracker == TimeStep::SUBSTEP) { return TimeStep::FAILED; } + // If we're already at the smallest allowable dt then there's nothing left to try + if (dt <= dt_min) { + return TimeStep::FAILED; + } // For the very first failure we want to save off the initial guessed time step if (num_failures == 0) { dt_orig = dt; diff --git a/src/solvers/mechanics_solver.cpp b/src/solvers/mechanics_solver.cpp index 4b35bb0..2902ba4 100644 --- a/src/solvers/mechanics_solver.cpp +++ b/src/solvers/mechanics_solver.cpp @@ -55,6 +55,61 @@ void ExaNewtonSolver::SetOperator(const std::shared_ptr op) c.UseDevice(true); } +/** + * @brief Per-iteration residual output shared by all nonlinear solvers + * + * @details print_level is set through SetPrintLevel() which sets it to -1 on all + * ranks other than rank 0, so only rank 0 ever prints anything here. + */ +void ExaNewtonSolver::PrintIterationInfo(int it, double norm, double norm0) const { + if (print_level < 1) { + return; + } + mfem::out << "Newton iteration " << std::setw(2) << it << " : ||r|| = " << norm; + if (it > 0) { + mfem::out << ", ||r||/||r_0|| = " << norm / norm0; + } + mfem::out << '\n'; +} + +/** + * @brief Step scale output shared by all nonlinear solvers + * + * @details Uses the same "Newton iteration N : " prefix as PrintIterationInfo so all + * per-iteration output follows a single "key = value" format across solvers. + */ +void ExaNewtonSolver::PrintStepScaleInfo(int it, double scale) const { + if (print_level < 1) { + return; + } + mfem::out << "Newton iteration " << std::setw(2) << it << " : step scale = " << scale << '\n'; +} + +/** + * @brief Final summary output shared by all nonlinear solvers + * + * @details Only used when the per-iteration output is turned off (print_level == 0). + */ +void ExaNewtonSolver::PrintSummaryInfo( + int it, double norm, double norm0, bool did_converge, int num_rejected) const { + if (print_level != 0) { + return; + } + // Guard against an initial residual of 0 which would converge right away + const double rel_norm = (norm0 > 0.0) ? norm / norm0 : 0.0; + const char* iter_str = (it == 1) ? " iteration" : " iterations"; + if (did_converge) { + mfem::out << "Newton solver converged in " << it << iter_str; + } else { + mfem::out << "Newton solver failed to converge after " << it << iter_str; + } + mfem::out << " : ||r|| = " << norm << ", ||r||/||r_0|| = " << rel_norm; + if (num_rejected >= 0) { + mfem::out << ", rejected steps = " << num_rejected; + } + mfem::out << '\n'; +} + /** * @brief Newton-Raphson iteration implementation * @@ -113,13 +168,7 @@ void ExaNewtonSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { for (it = 0; true; it++) { // Make sure the norm is finite MFEM_ASSERT_0(mfem::IsFinite(norm), "norm = " << norm); - if (print_level >= 0) { - mfem::out << "Newton iteration " << std::setw(2) << it << " : ||r|| = " << norm; - if (it > 0) { - mfem::out << ", ||r||/||r_0|| = " << norm / norm0; - } - mfem::out << '\n'; - } + PrintIterationInfo(it, norm, norm0); // See if our solution has converged and we can quit if (norm <= norm_max) { converged = 1; @@ -143,6 +192,7 @@ void ExaNewtonSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { break; } + PrintStepScaleInfo(it, c_scale); add(x, -c_scale, c, x); // full update to the current config // ExaConstit (srw) @@ -165,10 +215,6 @@ void ExaNewtonSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { if (norm_ratio > 5.0e-1) { scale = 0.5; - if (print_level >= 0) { - mfem::out << "The relaxation factor for the next iteration has been reduced to " - << scale << "\n"; - } } else { scale = 1.0; } @@ -176,6 +222,7 @@ void ExaNewtonSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { final_iter = it; final_norm = norm; + PrintSummaryInfo(it, norm, norm0, converged); } /** @@ -265,13 +312,7 @@ void ExaNewtonLSSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { for (it = 0; true; it++) { // Make sure the norm is finite MFEM_ASSERT_0(mfem::IsFinite(norm), "norm = " << norm); - if (print_level >= 0) { - mfem::out << "Newton iteration " << std::setw(2) << it << " : ||r|| = " << norm; - if (it > 0) { - mfem::out << ", ||r||/||r_0|| = " << norm / norm0; - } - mfem::out << '\n'; - } + PrintIterationInfo(it, norm, norm0); // See if our solution has converged and we can quit if (norm <= norm_max) { converged = 1; @@ -312,17 +353,18 @@ void ExaNewtonLSSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { double eps = (3.0 * q1 - 4.0 * q2 + q3) / (4.0 * (q1 - 2.0 * q2 + q3)); + // Smallest relaxation factor we'll allow the line search to take + constexpr double min_scale = 0.05; + if ((q1 - 2.0 * q2 + q3) > 0 && eps > 0 && eps < 1) { - scale = eps; + // The quadratic minimum can land arbitrarily close to 0, so make sure + // we don't go below our minimum relaxation factor + scale = std::max(eps, min_scale); } else if (q3 < q1) { scale = 1.0; } else { // We should probably just quit if this is the case... - scale = 0.05; - } - - if (print_level >= 0) { - mfem::out << "The relaxation factor for this iteration is " << scale << std::endl; + scale = min_scale; } x = x_prev; @@ -334,6 +376,7 @@ void ExaNewtonLSSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { break; } + PrintStepScaleInfo(it, c_scale); add(x, -c_scale, c, x); // full update to the current config // ExaConstit (srw) @@ -349,4 +392,5 @@ void ExaNewtonLSSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { final_iter = it; final_norm = norm; + PrintSummaryInfo(it, norm, norm0, converged); } \ No newline at end of file diff --git a/src/solvers/mechanics_solver.hpp b/src/solvers/mechanics_solver.hpp index 7396c79..a45e5a4 100644 --- a/src/solvers/mechanics_solver.hpp +++ b/src/solvers/mechanics_solver.hpp @@ -41,6 +41,50 @@ class ExaNewtonSolver : public mfem::IterativeSolver { /** @brief Pointer to the preconditioner */ std::shared_ptr prec_mech; + /** + * @brief Print the residual information for a single nonlinear iteration + * + * @param it Current nonlinear iteration number + * @param norm Current residual norm + * @param norm0 Initial residual norm + * + * @details Only prints when print_level >= 1. All nonlinear solvers use this so + * that the per-iteration output has the same format regardless of the solver. + */ + void PrintIterationInfo(int it, double norm, double norm0) const; + + /** + * @brief Print the scale applied to the Newton step taken from an iteration + * + * @param it Nonlinear iteration number the step is being taken from + * @param scale Fraction of the full Newton step that is being taken + * + * @details Only prints when print_level >= 1. Each solver provides its own + * notion of the scale (relaxation factor for NR, line search factor for NRLS, + * and the trust-region limited fraction of the Newton step for TRDOG), but it is + * always printed in the same format so the output can be parsed the same way + * regardless of the solver. A scale that is small and stays small is a sign + * that the solver is struggling. + */ + void PrintStepScaleInfo(int it, double scale) const; + + /** + * @brief Print a one line summary of the nonlinear solve + * + * @param it Number of nonlinear iterations that were run + * @param norm Final residual norm + * @param norm0 Initial residual norm + * @param did_converge Whether the nonlinear solve converged + * @param num_rejected Number of rejected trial steps for solvers that have them + * (e.g. trust-region). Not printed if negative. + * + * @details Only prints when print_level == 0. In this mode the per-iteration + * output is turned off, so this provides the basic information of the number of + * iterations, the final residual, and the final relative residual. + */ + void PrintSummaryInfo( + int it, double norm, double norm0, bool did_converge, int num_rejected = -1) const; + public: /** * @brief Default constructor diff --git a/src/solvers/trust_region_solver.cpp b/src/solvers/trust_region_solver.cpp new file mode 100644 index 0000000..0d486ff --- /dev/null +++ b/src/solvers/trust_region_solver.cpp @@ -0,0 +1,444 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other ExaConstit Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: MIT + +#include "solvers/trust_region_solver.hpp" + +#include "utilities/mechanics_log.hpp" +#include "utilities/unified_logger.hpp" + +#include "mfem.hpp" +#include "mfem/general/globals.hpp" +#include "mfem/linalg/linalg.hpp" + +#include +#include +#include +#include +#include + +/** + * @brief Compute the Powell dogleg step inside the trust region. + * + * @details Step-by-step algorithm: + * + * 1. **Full Newton step inside trust region**: + * If ||s_N|| <= delta, take the full Newton step. The predicted residual + * is zero (the linear model F + J*s_N = 0 is exactly satisfied). + * + * 2. **Cauchy point outside trust region**: + * Compute the Cauchy point parameters: + * - alpha = ||g||^2 / ||J*g||^2 (optimal scaling along steepest descent) + * - ||s_sd_opt|| = alpha * ||g|| (norm of the optimal Cauchy step) + * If ||s_sd_opt|| >= delta, the optimal Cauchy point is outside the trust + * region. Step along the steepest descent direction to the boundary: + * delx = -delta * g / ||g|| + * The predicted residual norm is computed from the linear model evaluated + * at this truncated Cauchy step. + * + * 3. **Dogleg interpolation (second leg)**: + * Otherwise, interpolate along the line segment from the Cauchy point to + * the Newton point, finding the parameter beta in [0, 1] such that the + * interpolated step lies on the trust-region boundary. The intersection + * is found by solving a quadratic: + * delx(beta) = beta * s_N - (1 - beta) * alpha * g + * ||delx(beta)||^2 = delta^2 + * yielding qa*beta^2 - 2*qb*beta + qc = 0 where: + * qa = ||p||^2, qb = alpha * (p . g), qc = ||s_sd_opt||^2 - delta^2 + * and p = s_N + alpha * g. + * Beta is taken from the larger root and clamped to [0, 1] for safety. + */ +void ExaTrustRegionSolver::Dogleg(double delta, + double res_0, + double nr_norm, + double Jg_2, + const mfem::Vector& grad, + const mfem::Vector& nrStep, + mfem::Vector& delx, + double& pred_resid, + DoglegStep& step, + double& beta) const { + beta = 0.0; + + // --- Case 1: Full Newton step fits inside the trust region --- + if (nr_norm <= delta) { + step = DoglegStep::NEWTON; + delx = nrStep; + pred_resid = 0.0; + return; + } + + // Cauchy point parameters using MPI-aware dot products + const double norm2_grad = Dot(grad, grad); + const double norm_grad = std::sqrt(norm2_grad); + + const double alpha = (Jg_2 > 0.0) ? (norm2_grad / Jg_2) : 1.0; + const double norm_grad_inv = (norm_grad > 0.0) ? (1.0 / norm_grad) : 1.0; + const double norm_s_sd_opt = alpha * norm_grad; + + // --- Case 2: Cauchy point is outside the trust region --- + // Take a step along the steepest descent direction to the trust-region boundary + if (norm_s_sd_opt >= delta) { + // delx = -delta * (grad / ||grad||) + const double factor = -delta * norm_grad_inv; + delx = grad; + delx *= factor; + + // Predicted residual from linear model at the truncated Cauchy step + const double val = -(delta * norm_grad) + + 0.5 * delta * delta * Jg_2 * (norm_grad_inv * norm_grad_inv); + pred_resid = std::sqrt(std::max(2.0 * val + res_0 * res_0, 0.0)); + step = DoglegStep::STEEPEST_DESCENT; + } + // --- Case 3: Cauchy inside, Newton outside; interpolate along the second leg --- + else { + // Reuse delx as workspace for p = nrStep + alpha * grad + mfem::Vector& p = delx; + add(nrStep, alpha, grad, p); + + // Quadratic coefficients for the trust-region boundary intersection + double qa = Dot(p, p); + double qb = Dot(p, grad) * alpha; + double qc = norm_s_sd_opt * norm_s_sd_opt - delta * delta; + + double discriminant = qb * qb - qa * qc; + beta = (qa > 0.0) ? (qb + std::sqrt(std::max(discriminant, 0.0))) / qa : 0.0; + + // Clamp beta to [0, 1] to handle any roundoff at the boundary + beta = std::max(0.0, std::min(1.0, beta)); + + // delx = beta * nrStep - (1 - beta) * alpha * grad + const double omb = 1.0 - beta; + const double omba = omb * alpha; + add(beta, nrStep, -omba, grad, delx); + + // Predicted residual from linear model at the dogleg step + const double res_cauchy = (Jg_2 > 0.0) + ? std::sqrt(std::max(res_0 * res_0 - alpha * norm2_grad, 0.0)) + : res_0; + pred_resid = omb * res_cauchy; + step = DoglegStep::DOGLEG; + } +} + +/** + * @brief Detailed per-iteration trust-region output (print_level > 1) + * + * @details Example output line: + * TR step: dogleg (beta = 0.42), predicted ||r|| = 1.2e-03, trial ||r|| = 1.3e-03, + * rho = 0.83, delta = 0.5 -> 1, accepted + * (printed as a single line) + */ +void ExaTrustRegionSolver::PrintStepDetails(DoglegStep step, + double beta, + double pred_resid, + double trial_resid, + double rho, + double delta_old, + double delta_new, + const char* outcome) const { + if (print_level < 2) { + return; + } + mfem::out << " TR step: "; + switch (step) { + case DoglegStep::NEWTON: + mfem::out << "Newton"; + break; + case DoglegStep::STEEPEST_DESCENT: + mfem::out << "steepest descent"; + break; + case DoglegStep::DOGLEG: + mfem::out << "dogleg (beta = " << beta << ")"; + break; + } + mfem::out << ", predicted ||r|| = " << pred_resid << ", trial ||r|| = " << trial_resid + << ", rho = "; + if (std::isnan(rho)) { + mfem::out << "n/a"; + } else { + mfem::out << rho; + } + mfem::out << ", delta = " << delta_old; + if (delta_new != delta_old) { + mfem::out << " -> " << delta_new; + } + mfem::out << ", " << outcome << '\n'; +} + +/** + * @brief Trust-region dogleg Newton iteration implementation. + * + * @details Step-by-step algorithm for solving F(x) = b: + * + * **Initial setup**: + * 1. Validate that operator (oper_mech), preconditioner (prec_mech), and + * delta_ctrl are properly configured + * 2. Allocate all device-aware working vectors (nrStep, grad, delx, Jg_temp, + * x_prev) once before the iteration loop + * 3. Evaluate initial residual r = F(x) - b and compute its norm + * 4. Set the convergence threshold norm_max = max(rel_tol * res, abs_tol) + * 5. Initialize trust-region radius delta from delta_ctrl.deltaInit + * + * **Main iteration loop** (until convergence or max_iter accepted iterations): + * 1. If the previous step was *not* rejected, recompute Newton machinery: + * a. Get Jacobian J = oper_mech->GetGradient(x). The material state is + * consistent with x because Mult(x, r) was just evaluated. + * b. Compute steepest descent: grad = J^T * r (gradient of f = 0.5 ||F||^2) + * c. Compute Jg_2 = ||J * grad||^2 for the optimal Cauchy step length + * d. Solve the Newton system J*c = r via the Krylov solver (prec_mech), + * then negate: nrStep = -c. The negation matches SNLS convention where + * the Newton update is x += nrStep (whereas ExaNewtonSolver uses x -= c). + * e. Compute nr_norm = ||nrStep|| + * If the previous step *was* rejected, all of this data is still valid + * from the last accepted iteration and we just recompute the dogleg with + * the smaller delta. + * 2. Save x_prev = x for potential rollback on rejection + * 3. Compute the dogleg step delx via Dogleg() helper + * 4. Apply the trial step: x = x_prev + delx + * 5. Evaluate residual at the trial point: r = F(x) - b + * 6. Check convergence: if ||r|| <= norm_max, accept and exit + * 7. Update delta via delta_ctrl.UpdateDelta() based on actual vs predicted + * reduction. This may also flag the step for rejection. + * 8. If rejected: restore x = x_prev, restore residual norm, set reject_prev. + * The material state inside the model handles itself analogously to the + * ExaNewtonLSSolver line-search behavior — when Mult() is called again at + * the next trial point, the model recomputes from the beginning-step state. + * Rejected steps do not count as iterations (unlike SNLS) since they are + * retries of the same iteration, much like the trial evaluations of a line + * search. An internal cap on consecutive rejections guards against an + * endless loop, although deltaMin should normally be reached first. + * 9. If accepted: the iteration count is incremented and the step scale and + * new residual are printed in the same format as the other solvers. + * + * **Performance Profiling**: + * - "TR_dogleg_solver" scope for overall trust-region solver performance + * - "TR_newton_setup" scope for J^T*r and J*g computations + * - "TR_gradient_transpose" scope for the J^T*r call specifically + * - "TR_newton_solve" scope for the Krylov inner solve + * - "TR_trial_eval" scope for residual evaluations at trial points + * - "krylov_solver" scope for the actual Krylov solver call + * + * @note All scalar quantities (norms, dot products) use MFEM's MPI-aware + * Norm() and Dot() functions through the IterativeSolver base class + */ +void ExaTrustRegionSolver::Mult(const mfem::Vector& b, mfem::Vector& x) const { + CALI_CXX_MARK_SCOPE("TR_dogleg_solver"); + MFEM_ASSERT_0(oper_mech, "the Operator is not set (use SetOperator)."); + MFEM_ASSERT_0(prec_mech, "the Solver is not set (use SetSolver)."); + MFEM_ASSERT(delta_ctrl.Validate(), "TrDeltaControl parameters are invalid."); + + const bool have_b = (b.Size() == Height()); + + // --- Allocate working vectors once, reused across iterations --- + mfem::Vector nrStep(width, mfem::Device::GetMemoryType()); + mfem::Vector grad(width, mfem::Device::GetMemoryType()); + mfem::Vector delx(width, mfem::Device::GetMemoryType()); + mfem::Vector Jg_temp(width, mfem::Device::GetMemoryType()); + mfem::Vector x_prev(width, mfem::Device::GetMemoryType()); + + nrStep.UseDevice(true); + grad.UseDevice(true); + delx.UseDevice(true); + Jg_temp.UseDevice(true); + x_prev.UseDevice(true); + + // --- Initial residual evaluation: r = F(x) - b --- + oper_mech->Mult(x, r); + if (have_b) { + r -= b; + } + + double res = Norm(r); + // Residual norm at the start of the solve used for the relative residual + const double norm0 = res; + // Residual norm at the last accepted state + double res_0 = res; + const double norm_max = std::max(rel_tol * res, abs_tol); + + MFEM_ASSERT_0(mfem::IsFinite(res), "norm = " << res); + PrintIterationInfo(0, res, norm0); + + if (res <= norm_max) { + converged = true; + final_iter = 0; + final_norm = res; + PrintSummaryInfo(0, res, norm0, true, 0); + return; + } + + // --- Initialize trust-region state --- + double delta = delta_ctrl.deltaInit; + double rho = 0.0; + bool reject_prev = false; + + // Persisted across iterations when a step is not rejected + double Jg_2 = 0.0; + double nr_norm = 0.0; + + // Only accepted steps count as iterations. A rejected trial step is retried from + // the same state with a smaller trust region, reusing the Jacobian and Newton step, + // so it is treated like the extra residual evaluations of a line search rather than + // a full iteration. This keeps max_iter, final_iter, and the per-iteration output + // consistent with the other nonlinear solvers. Note, this differs from SNLS where + // rejected steps also count towards the iteration count. + int it = 0; + int num_rejected = 0; + int num_consecutive_rejected = 0; + // Internal safeguard against endlessly rejecting steps. In practice the trust region + // radius should reach deltaMin well before this is hit. + constexpr int max_consecutive_rejected = 50; + converged = false; + + // --- Main iteration loop --- + while (it < max_iter) { + // If the previous step was not rejected, recompute Newton direction + // and steepest descent direction at the current x. The Jacobian data + // is current because oper_mech->Mult(x, r) was just called. + if (!reject_prev) { + CALI_CXX_MARK_SCOPE("TR_newton_setup"); + + mfem::Operator& J = oper_mech->GetGradient(x); + + // Steepest descent direction: grad = J^T * r + // This is the gradient of the merit function f(x) = 0.5 * ||F(x)||^2 + { + CALI_CXX_MARK_SCOPE("TR_gradient_transpose"); + J.MultTranspose(r, grad); + } + + // Compute ||J * grad||^2 for the optimal Cauchy step length + // alpha_cauchy = ||grad||^2 / ||J*grad||^2 + { + J.Mult(grad, Jg_temp); + Jg_2 = Dot(Jg_temp, Jg_temp); + } + + // Solve Newton system: J * c = r, then nrStep = -c + // CGSolver follows the same convention as ExaNewtonSolver where the + // Krylov solve produces c such that the Newton update would be x -= c. + // For the dogleg we need nrStep = -J^{-1}*r, so we negate after the solve. + { + CALI_CXX_MARK_SCOPE("TR_newton_solve"); + c = 0.0; + this->CGSolver(J, r, c); + nrStep = c; + nrStep.Neg(); + } + + nr_norm = Norm(nrStep); + } + + // Save state for potential step rejection + x_prev = x; + + // Compute the dogleg step + double pred_resid = 0.0; + double beta = 0.0; + DoglegStep step = DoglegStep::NEWTON; + Dogleg(delta, res_0, nr_norm, Jg_2, grad, nrStep, delx, pred_resid, step, beta); + const bool use_nr = (step == DoglegStep::NEWTON); + // Fraction of the full Newton step being taken. This is the trust-region + // analog of the NR / NRLS relaxation factor. When not taking the Newton step, + // the step lies on the trust-region boundary so ||delx|| = delta < nr_norm. + const double step_scale = use_nr ? 1.0 : delta / nr_norm; + + // Apply the trial step: x = x_prev + delx + x = x_prev; + x += delx; + + // Evaluate residual at the trial point + reject_prev = false; + { + CALI_CXX_MARK_SCOPE("TR_trial_eval"); + oper_mech->Mult(x, r); + if (have_b) { + r -= b; + } + } + + res = Norm(r); + MFEM_ASSERT_0(mfem::IsFinite(res), "norm = " << res); + + // Check convergence + if (res <= norm_max) { + converged = true; + PrintStepDetails(step, + beta, + pred_resid, + res, + std::numeric_limits::quiet_NaN(), + delta, + delta, + "converged"); + PrintStepScaleInfo(it, step_scale); + it++; + PrintIterationInfo(it, res, norm0); + break; + } + + // Update delta from actual vs predicted reduction. May flag for rejection. + // rho is only computed if the predicted change is non-zero. + const double delta_old = delta; + rho = std::numeric_limits::quiet_NaN(); + bool delta_ok = delta_ctrl.UpdateDelta( + delta, res, res_0, pred_resid, reject_prev, use_nr, nr_norm, rho); + + PrintStepDetails(step, + beta, + pred_resid, + res, + rho, + delta_old, + delta, + reject_prev ? "rejected" : "accepted"); + + if (reject_prev) { + // Revert x and the residual norm. On the next pass reject_prev == true so + // we skip the Newton solve and recompute the dogleg with the updated + // (smaller) delta. The Jacobian, grad, nrStep, and Jg_2 are still valid + // from the last accepted state. Nothing is printed at the standard output + // level since this is the same iteration being retried. + x = x_prev; + res = res_0; + num_rejected++; + num_consecutive_rejected++; + } else { + // Accepted step so this completes an iteration + PrintStepScaleInfo(it, step_scale); + it++; + PrintIterationInfo(it, res, norm0); + num_consecutive_rejected = 0; + } + + if (!delta_ok) { + if (print_level > 0) { + if (delta <= delta_ctrl.deltaMin) { + mfem::out << "The trust region radius reached its minimum value of " << delta + << " so the nonlinear solve is stopping\n"; + } else { + mfem::out << "The trust region model predicted no change in the residual at " + "its maximum radius so the nonlinear solve is stopping\n"; + } + } + converged = false; + break; + } + + if (num_consecutive_rejected >= max_consecutive_rejected) { + if (print_level > 0) { + mfem::out << "The trust region step was rejected " << num_consecutive_rejected + << " times in a row so the nonlinear solve is stopping\n"; + } + converged = false; + break; + } + + res_0 = res; + } + + final_iter = it; + final_norm = res; + PrintSummaryInfo(it, res, norm0, converged, num_rejected); +} \ No newline at end of file diff --git a/src/solvers/trust_region_solver.hpp b/src/solvers/trust_region_solver.hpp new file mode 100644 index 0000000..c98de41 --- /dev/null +++ b/src/solvers/trust_region_solver.hpp @@ -0,0 +1,388 @@ +// Copyright (c) 2017-2025, Lawrence Livermore National Security, LLC and +// other ExaConstit Project Developers. See the top-level LICENSE file for details. +// +// SPDX-License-Identifier: MIT +#pragma once + +#include "solvers/mechanics_solver.hpp" + +#include "mfem.hpp" +#include "mfem/linalg/solvers.hpp" + +#include +#include +#include + +/** + * @brief Trust-region radius control parameters for the dogleg solver. + * + * @details Ported from SNLS's TrDeltaControl. Controls how the trust-region + * radius delta is updated based on the ratio rho = actual_reduction / predicted_reduction. + * + * The update logic: + * - If rho is in the "good" band [xiLG, xiUG] and the step reduced the residual, + * increase delta (unless the full Newton step was taken) + * - If rho is outside the "ok" band [xiLO, xiUO], decrease delta + * - If the predicted change is zero and delta is not at max, force a small increase + * - If the residual actually increased, reject the step + * + * @ingroup ExaConstit_solvers + */ +struct TrDeltaControl { + /// @brief Lower bound of the "good" rho interval (increase delta when rho > xiLG) + double xiLG = 0.75; + /// @brief Upper bound of the "good" rho interval + double xiUG = 1.4; + /// @brief Factor by which to increase delta + double xiIncDelta = 1.5; + /// @brief Lower bound of the "ok" rho interval (decrease delta when rho < xiLO) + double xiLO = 0.35; + /// @brief Upper bound of the "ok" rho interval (decrease delta when rho > xiUO) + double xiUO = 5.0; + /// @brief Factor by which to decrease delta + double xiDecDelta = 0.25; + /// @brief Forced increase factor when predicted change is zero + double xiForcedIncDelta = 1.2; + /// @brief Initial trust-region radius + double deltaInit = 1.0; + /// @brief Minimum allowed trust-region radius (solver fails if hit) + double deltaMin = 1e-12; + /// @brief Maximum allowed trust-region radius + double deltaMax = 1e4; + /// @brief Whether to reject steps that increase the residual + bool rejectResIncrease = true; + + /** + * @brief Validate that the control parameters are self-consistent. + * + * @return true if all parameter relationships are valid, false otherwise + * + * Verifies the following invariants: + * - deltaMin > 0 and deltaMax > deltaMin + * - The "good" rho band [xiLG, xiUG] sits inside the "ok" band [xiLO, xiUO] + * - The increase factor (xiIncDelta) is greater than 1 + * - The decrease factor (xiDecDelta) is in (0, 1) + * - The forced-increase factor is greater than 1 + */ + bool Validate() const { + return (deltaMin > 0.0) && (deltaMax > deltaMin) && (xiLG > xiLO) && (xiUG < xiUO) && + (xiIncDelta > 1.0) && (xiDecDelta > 0.0 && xiDecDelta < 1.0) && + (xiForcedIncDelta > 1.0); + } + + /** + * @brief Decrease the trust-region radius after a rejected/poor step. + * + * @param[in,out] delta Current radius, modified on output + * @param[in] norm_full Norm of the full Newton step + * @param[in] took_full Whether the full Newton step was used at the last iteration + * @return true if delta is still above deltaMin, false if solver should fail + * + * @details If the full Newton step was taken, uses a geometric mean blend of + * the current delta and the Newton step norm scaled by xiDecDelta. Otherwise + * just multiplies delta by xiDecDelta. Returns false (and sets delta to deltaMin) + * if the resulting delta drops below the minimum allowed value. + */ + bool DecrDelta(double& delta, double norm_full, bool took_full) const { + if (took_full) { + double tempa = delta * xiDecDelta; + double tempb = norm_full * xiDecDelta; + delta = std::sqrt(tempa * tempb); + } else { + delta *= xiDecDelta; + } + + if (delta < deltaMin) { + delta = deltaMin; + return false; + } + return true; + } + + /** + * @brief Increase the trust-region radius after a successful step. + * + * @param[in,out] delta Current radius, modified on output + * + * @details Multiplies delta by xiIncDelta and clamps at deltaMax. + */ + void IncrDelta(double& delta) const { + delta *= xiIncDelta; + if (delta > deltaMax) { + delta = deltaMax; + } + } + + /** + * @brief Update trust-region radius based on actual vs predicted residual change. + * + * @param[in,out] delta Trust-region radius, modified on output + * @param[in] res New residual norm (after the candidate step) + * @param[in] res_0 Previous residual norm (before the candidate step) + * @param[in] pred_resid Predicted residual norm from the dogleg model + * @param[out] reject Whether the step should be rejected (residual increased) + * @param[in] took_full Whether the full Newton step was taken + * @param[in] norm_full Norm of the full Newton step + * @param[out] rho Actual / predicted reduction ratio (output for diagnostics). + * Left untouched if the predicted change is zero. + * @return true if the delta update succeeded, false if the solver should fail + * + * @details Algorithm (ported from SNLS TrDeltaControl::updateDelta): + * 1. Compute actual_change = res - res_0 and pred_change = pred_resid - res_0 + * 2. If pred_change is exactly zero, force delta larger (or fail if at max) + * 3. Otherwise compute rho = actual_change / pred_change + * 4. If rho is in the "good" band [xiLG, xiUG] and the residual decreased, + * increase delta (unless the full Newton step was already taken) + * 5. If rho is outside the "ok" band [xiLO, xiUO], decrease delta + * 6. If the residual increased and rejectResIncrease is set, mark for rejection + * 7. If a rejected step was the full Newton step, keep decreasing delta until + * the full Newton step no longer fits in the trust region. Otherwise, the + * retried step would be identical to the rejected one. (Not in SNLS) + */ + bool UpdateDelta(double& delta, + double res, + double res_0, + double pred_resid, + bool& reject, + bool took_full, + double norm_full, + double& rho) const { + bool success = true; + double actual_change = res - res_0; + double pred_change = pred_resid - res_0; + + if (pred_change == 0.0) { + if (delta >= deltaMax) { + success = false; + } else { + delta = std::min(delta * xiForcedIncDelta, deltaMax); + } + } else { + rho = actual_change / pred_change; + + if ((rho > xiLG) && (actual_change < 0.0) && (rho < xiUG)) { + // Step is in the "good" band and residual actually decreased + if (!took_full) { + IncrDelta(delta); + } + } else if ((rho < xiLO) || (rho > xiUO)) { + // Step quality is outside the acceptable band; shrink delta + success = DecrDelta(delta, norm_full, took_full); + } + } + + reject = false; + // Do not make this >=, may have res and res_0 both zero and that is ok + if ((actual_change > 0.0) && rejectResIncrease) { + reject = true; + } + + // A rejected full Newton step is retried from the same state with the same + // Newton step. If delta is still >= norm_full, the full Newton step still fits + // inside the trust region and the retry would be identical to the step that was + // just rejected, wasting a residual evaluation only to be rejected again. The + // took_full branch of DecrDelta does not guarantee delta < norm_full after a + // single call, since it uses the geometric mean of delta and norm_full. + // So, keep decreasing delta until the retried step actually differs. This + // results in the same delta as evaluating those identical retries would have, + // just without the wasted residual evaluations. DecrDelta strictly decreases + // delta here and its repeated application converges to xiDecDelta^2 * norm_full, + // so this loop always terminates. + if (reject && took_full) { + while (success && (delta >= norm_full)) { + success = DecrDelta(delta, norm_full, took_full); + } + } + + return success; + } +}; + +/** + * @brief Which part of the dogleg path a trust-region step was taken along. + */ +enum class DoglegStep { + NEWTON, ///< Full Newton step fit inside the trust region + STEEPEST_DESCENT, ///< Step along the steepest descent direction to the trust-region boundary + DOGLEG ///< Step between the Cauchy point and the Newton point (second leg) +}; + +/** + * @brief Trust-region dogleg solver for nonlinear solid mechanics problems. + * + * @details This class implements a Powell-dogleg trust-region method for solving + * nonlinear systems F(x) = b. It extends ExaNewtonSolver and reuses the same + * Krylov solver infrastructure (prec_mech) for computing the Newton direction. + * + * The trust-region method augments standard Newton with a globalization strategy + * that interpolates between the steepest descent direction and the full Newton + * step, constrained to a trust-region radius delta. Step quality is monitored + * via the ratio rho = actual_reduction / predicted_reduction, and delta is + * adjusted up or down accordingly. + * + * This is a direct port of SNLS's SNLSTrDlDenseG solver, lifted from the + * material-point dense system to the global FE system. + * + * Algorithm at each iteration: + * 1. Compute steepest descent direction g = J^T * r (gradient of merit f = 0.5 ||F||^2) + * 2. Compute ||J*g||^2 for the optimal Cauchy step length + * 3. Solve J * c = r for the full Newton direction (using prec_mech Krylov solver) + * 4. Compute the dogleg step within the trust region + * 5. Evaluate the residual at the trial point + * 6. Accept or reject based on the rho ratio; update delta accordingly + * + * Requirements: + * - The gradient operator must support MultTranspose (for J^T*r computation). + * This means the assembly mode must be EA, FA, or PA with the native PA + * transpose kernels enabled. + * + * @ingroup ExaConstit_solvers + */ +class ExaTrustRegionSolver : public ExaNewtonSolver { +public: + /** + * @brief Default constructor + * + * @details Creates an ExaTrustRegionSolver instance for single-processor + * execution. The operator and linear solver must be set separately using + * SetOperator() and SetSolver(), and the trust-region control parameters + * may be customized via SetTrustRegionControl(). + */ + ExaTrustRegionSolver() {} + +#ifdef MFEM_USE_MPI + /** + * @brief MPI constructor + * + * @param _comm MPI communicator for parallel execution + * + * @details Creates an ExaTrustRegionSolver instance for parallel execution + * using the specified MPI communicator. All trust-region scalar quantities + * (norms, dot products) use MPI-aware reductions through MFEM's Dot/Norm. + */ + ExaTrustRegionSolver(MPI_Comm _comm) : ExaNewtonSolver(_comm) {} +#endif + + /** @brief Use parent class SetOperator methods */ + using ExaNewtonSolver::SetOperator; + + /** @brief Use parent class SetSolver methods */ + using ExaNewtonSolver::SetSolver; + + /** @brief Use parent class CGSolver method (Krylov solve wrapper) */ + using ExaNewtonSolver::CGSolver; + + /** + * @brief Set trust-region control parameters. + * + * @param ctrl TrDeltaControl struct with all tuning parameters + * + * @details Replaces the internal control parameters with a user-supplied + * configuration. Typically called after construction (and before Mult()) + * to wire up parameters parsed from the TOML configuration file. + */ + void SetTrustRegionControl(const TrDeltaControl& ctrl) { + delta_ctrl = ctrl; + } + + /** + * @brief Get a mutable reference to the trust-region control parameters. + * @return Reference to the internal TrDeltaControl + */ + TrDeltaControl& GetTrustRegionControl() { + return delta_ctrl; + } + + /** + * @brief Get a const reference to the trust-region control parameters. + * @return Const reference to the internal TrDeltaControl + */ + const TrDeltaControl& GetTrustRegionControl() const { + return delta_ctrl; + } + + /** + * @brief Solve the nonlinear system F(x) = b using trust-region dogleg method. + * + * @param b Right-hand side vector (if b.Size() != Height(), assumes b = 0) + * @param x Solution vector (input: initial guess, output: converged solution) + * + * @details Implements the trust-region dogleg algorithm. See class-level + * documentation for the algorithm description. The Newton direction is + * computed by the Krylov solver wired in via SetSolver(); J^T*r is + * computed by calling MultTranspose() on the gradient operator. + * + * @pre SetOperator() and SetSolver() must be called before Mult() + * @pre The gradient operator must support MultTranspose (EA/FA mode, or + * PA mode with native transpose kernels) + * + * @post final_iter contains the number of accepted iterations performed + * (rejected trial steps are retries of the same iteration and do not count) + * @post final_norm contains the final residual norm + * @post converged flag indicates whether the solver converged + */ + virtual void Mult(const mfem::Vector& b, mfem::Vector& x) const; + +private: + /** + * @brief Compute the dogleg step given the current trust-region radius. + * + * @param[in] delta Trust-region radius + * @param[in] res_0 Current residual norm + * @param[in] nr_norm Norm of the full Newton step + * @param[in] Jg_2 ||J*g||^2 where g is the steepest descent direction + * @param[in] grad Steepest descent direction g = J^T * r + * @param[in] nrStep Full Newton step + * @param[out] delx The computed dogleg step + * @param[out] pred_resid Predicted residual norm after the step + * @param[out] step Which part of the dogleg path the step was taken along + * @param[out] beta Interpolation factor along the second leg (only meaningful + * when step == DoglegStep::DOGLEG) + * + * @details Ported from SNLS's dogleg() kernel. The dogleg path interpolates + * between the steepest descent direction (Cauchy point) and the full Newton + * step. Three cases are handled: + * - Newton step inside delta: take full Newton step + * - Cauchy point outside delta: step along steepest descent to boundary + * - Cauchy inside, Newton outside: solve quadratic for the dogleg leg + * intersection with the trust-region boundary + */ + void Dogleg(double delta, + double res_0, + double nr_norm, + double Jg_2, + const mfem::Vector& grad, + const mfem::Vector& nrStep, + mfem::Vector& delx, + double& pred_resid, + DoglegStep& step, + double& beta) const; + + /** + * @brief Print the detailed trust-region information for an iteration + * + * @param step Which part of the dogleg path the step was taken along + * @param beta Interpolation factor along the second leg of the dogleg path + * @param pred_resid Predicted residual norm from the dogleg model + * @param trial_resid Actual residual norm at the trial point + * @param rho Actual / predicted reduction ratio (NaN if it was not computed) + * @param delta_old Trust-region radius used for this step + * @param delta_new Trust-region radius for the next step + * @param outcome Short description of what happened to the step + * + * @details Only prints when print_level > 1. Everything about a trial step, + * including rejected ones, is provided on a single line. These lines are printed + * after the residual line of the iteration the trial steps are taken from. + */ + void PrintStepDetails(DoglegStep step, + double beta, + double pred_resid, + double trial_resid, + double rho, + double delta_old, + double delta_new, + const char* outcome) const; + + /// @brief Trust-region control parameters (mutable to allow tuning) + mutable TrDeltaControl delta_ctrl; +}; \ No newline at end of file diff --git a/src/system_driver.cpp b/src/system_driver.cpp index 15f4e2b..0a73f1b 100644 --- a/src/system_driver.cpp +++ b/src/system_driver.cpp @@ -3,15 +3,17 @@ #include "boundary_conditions/BCData.hpp" #include "boundary_conditions/BCManager.hpp" +#include "solvers/trust_region_solver.hpp" #include "utilities/mechanics_kernels.hpp" #include "utilities/mechanics_log.hpp" #include "utilities/unified_logger.hpp" -#include "ECMech_const.h" -#include "RAJA/RAJA.hpp" #include "mfem.hpp" #include "mfem/general/forall.hpp" +#include "ECMech_const.h" +#include "RAJA/RAJA.hpp" + #include #include @@ -45,6 +47,13 @@ void DirBdrFunc(int attr_id, mfem::Vector& y) { namespace { +void GetTrueDofsParallel(const mfem::ParGridFunction& gf, mfem::Vector& true_dofs) { + // used to do something like: + // gf.GetTrueDofs(true_dofs); + // but looks like there are issues with that on the GPUs with newer versions of MFEM + gf.ParallelAverage(true_dofs); +} + /** * @brief Helper function to find mesh bounding box for velocity gradient calculations * @@ -290,29 +299,11 @@ SystemDriver::SystemDriver(std::shared_ptr sim_state) } else { if (linear_solvers.preconditioner == PreconditionerType::AMG) { auto prec_amg = std::make_shared(); - HYPRE_Solver h_amg = static_cast(*prec_amg); - HYPRE_Real st_val = 0.90; - HYPRE_Real rt_val = -10.0; - // HYPRE_Real om_val = 1.0; - // - [[maybe_unused]] int ml = HYPRE_BoomerAMGSetMaxLevels(h_amg, 30); - ml = HYPRE_BoomerAMGSetCoarsenType(h_amg, 0); - ml = HYPRE_BoomerAMGSetMeasureType(h_amg, 0); - ml = HYPRE_BoomerAMGSetStrongThreshold(h_amg, st_val); - ml = HYPRE_BoomerAMGSetNumSweeps(h_amg, 3); - ml = HYPRE_BoomerAMGSetRelaxType(h_amg, 8); - // int rwt = HYPRE_BoomerAMGSetRelaxWt(h_amg, rt_val); - // int ro = HYPRE_BoomerAMGSetOuterWt(h_amg, om_val); - // Dimensionality of our problem - ml = HYPRE_BoomerAMGSetNumFunctions(h_amg, 3); - ml = HYPRE_BoomerAMGSetSmoothType(h_amg, 6); - ml = HYPRE_BoomerAMGSetSmoothNumLevels(h_amg, 3); - ml = HYPRE_BoomerAMGSetSmoothNumSweeps(h_amg, 3); - ml = HYPRE_BoomerAMGSetVariant(h_amg, 0); - ml = HYPRE_BoomerAMGSetOverlap(h_amg, 0); - ml = HYPRE_BoomerAMGSetDomainType(h_amg, 1); - ml = HYPRE_BoomerAMGSetSchwarzRlxWeight(h_amg, rt_val); - + const int problem_dim = m_sim_state->GetMesh()->SpaceDimension(); + const bool order_bynodes = (fe_space->GetOrdering() == mfem::Ordering::byNODES); + // Use MFEM's supported systems-AMG configuration so Hypre sees + // the correct vector-valued DOF ordering on newer MFEM/Hypre builds. + prec_amg->SetSystemsOptions(problem_dim, order_bynodes); prec_amg->SetPrintLevel(linear_solvers.print_level); J_prec = prec_amg; } else if (linear_solvers.preconditioner == PreconditionerType::ILU) { @@ -361,13 +352,49 @@ SystemDriver::SystemDriver(std::shared_ptr sim_state) } else if (nonlinear_solver.nl_solver == NonlinearSolverType::NRLS) { newton_solver = std::make_unique( m_sim_state->GetMeshParFiniteElementSpace()->GetComm()); + } else if (nonlinear_solver.nl_solver == NonlinearSolverType::TRDOG) { + // Build the trust-region dogleg solver and configure delta-control + // parameters from the parsed TOML options. If the user did not supply + // a [trust_region] sub-table, the solver's internal defaults (matching + // SNLS's TrDeltaControl defaults) are used. + auto tr_solver = std::make_unique( + m_sim_state->GetMeshParFiniteElementSpace()->GetComm()); + + if (nonlinear_solver.trust_region.has_value()) { + const auto& tr_opts = nonlinear_solver.trust_region.value(); + TrDeltaControl ctrl; + ctrl.deltaInit = tr_opts.delta_init; + ctrl.deltaMin = tr_opts.delta_min; + ctrl.deltaMax = tr_opts.delta_max; + ctrl.xiLG = tr_opts.xi_lg; + ctrl.xiUG = tr_opts.xi_ug; + ctrl.xiLO = tr_opts.xi_lo; + ctrl.xiUO = tr_opts.xi_uo; + ctrl.xiIncDelta = tr_opts.xi_inc; + ctrl.xiDecDelta = tr_opts.xi_dec; + ctrl.xiForcedIncDelta = tr_opts.xi_forced_inc; + ctrl.rejectResIncrease = tr_opts.reject_increase; + tr_solver->SetTrustRegionControl(ctrl); + } + + newton_solver = std::move(tr_solver); + + // Sanity check: TRDOG requires gradient transpose support (J^T*r). For + // PA mode, this requires the native PA transpose kernels in the + // integrator. EA and FULL always support transpose. We warn rather than + // hard-fail here because PA support exists once the kernels are wired. + if (options.solvers.assembly == AssemblyType::PA) { + mfem::out << "Note: TRDOG with PA assembly requires native PA transpose " + << "kernels in the gradient operator.\n"; + } } // Set the newton solve parameters newton_solver->iterative_mode = true; newton_solver->SetSolver(J_solver); newton_solver->SetOperator(mech_operator); - newton_solver->SetPrintLevel(1); + // SetPrintLevel also makes sure only rank 0 prints the nonlinear solver output + newton_solver->SetPrintLevel(nonlinear_solver.print_info); newton_solver->SetRelTol(nonlinear_solver.rel_tol); newton_solver->SetAbsTol(nonlinear_solver.abs_tol); newton_solver->SetMaxIter(nonlinear_solver.iter); @@ -381,6 +408,8 @@ const mfem::Array& SystemDriver::GetEssTDofList() { void SystemDriver::Solve() { mfem::Vector zero; auto x = m_sim_state->GetPrimalField(); + // Global convergence status across all ranks + bool converged = false; if (auto_time) { // This would only happen on the last time step const auto x_prev = m_sim_state->GetPrimalFieldPrev(); @@ -419,23 +448,29 @@ void SystemDriver::Solve() { MPI_Allreduce(&succeed_t, &succeed, 1, MPI_C_BOOL, MPI_LAND, MPI_COMM_WORLD); state = m_sim_state->UpdateDeltaTime(newton_solver->GetNumIterations(), succeed); } // Do final converge check outside of this while loop + if (state == TimeStep::FAILED) { + MFEM_WARNING_0("Solution did not converge and the minimum dt or maximum number " + "of retries has been reached"); + } } + converged = succeed; } else { // We provide an initial guess for what our current coordinates will look like // based on what our last time steps solution was for our velocity field. // The end nodes are updated before the 1st step of the solution here so we're good. newton_solver->Mult(zero, *x); m_sim_state->UpdateDeltaTime(newton_solver->GetNumIterations(), true); + converged = newton_solver->GetConverged(); } // Just gotta be safe incase something in the solver wasn't playing nice and didn't swap things // back to the current configuration... // Once the system has finished solving, our current coordinates configuration are based on what // our converged velocity field ended up being equal to. - if (m_sim_state->GetMPIID() == 0 && newton_solver->GetConverged()) { + if (m_sim_state->GetMPIID() == 0 && converged) { ess_bdr_func->SetTime(m_sim_state->GetTime()); } - MFEM_VERIFY_0(newton_solver->GetConverged(), "Newton Solver did not converge."); + MFEM_VERIFY_0(converged, "Newton Solver did not converge."); } // Solve the Newton system for the 1st time step @@ -498,7 +533,7 @@ void SystemDriver::UpdateVelocity() { // pulled off the // VectorFunctionRestrictedCoefficient // populate the solution vector, v_sol, with the true dofs entries in v_cur. - velocity->GetTrueDofs(*vel_tdofs); + GetTrueDofsParallel(*velocity, *vel_tdofs); } if (ess_bdr["ess_vgrad"].Sum() > 0) { @@ -587,7 +622,7 @@ void SystemDriver::UpdateVelocity() { mfem::Vector vel_tdof_tmp(*vel_tdofs); vel_tdof_tmp.UseDevice(true); vel_tdof_tmp = 0.0; - velocity->GetTrueDofs(vel_tdof_tmp); + GetTrueDofsParallel(*velocity, vel_tdof_tmp); mfem::Array ess_tdofs(mech_operator->GetEssentialTrueDofs()); if (!mono_def_flag) { diff --git a/src/utilities/mechanics_kernels.cpp b/src/utilities/mechanics_kernels.cpp index 3a81fbd..e555dd0 100644 --- a/src/utilities/mechanics_kernels.cpp +++ b/src/utilities/mechanics_kernels.cpp @@ -104,5 +104,60 @@ void GradCalc(const int nqpts, } }); } // end GradCalc + +void ElementAveragedShapeGradients(const int nqpts, + const int nelems, + const int nnodes, + const double* jacobian_data, + const double* loc_grad_data, + const double* weights, + double* bbar_data, + const int* const local2global) { + const std::ptrdiff_t jac_elem_stride = 9 * static_cast(nqpts); + const int dshape_qpt_stride = 3 * nnodes; + const int bbar_elem_stride = 3 * nnodes; + mfem::forall(nelems, [=] MFEM_HOST_DEVICE(int i_local) { + // Input arrays are indexed by the global element, output by the local one. + const int i_input = local2global ? local2global[i_local] : i_local; + const double* jac_elem = jacobian_data + i_input * jac_elem_stride; + double* bbar = bbar_data + static_cast(i_local) * bbar_elem_stride; + for (int i = 0; i < bbar_elem_stride; ++i) { + bbar[i] = 0.0; + } + double volume = 0.0; + for (int q = 0; q < nqpts; ++q) { + BBarAccumulateQpt(nnodes, + jac_elem + 9 * q, + loc_grad_data + q * dshape_qpt_stride, + weights[q], + bbar, + volume); + } + BBarFinalize(nnodes, volume, bbar); + }); +} + +void ApplyBBarVelocityGradient(const int nqpts, + const int nelems, + const int nnodes, + const double* bbar_data, + const double* vel_data, + double* vel_grad_data, + const int* const local2global) { + const int elem_stride = 3 * nnodes; + const std::ptrdiff_t vgrad_elem_stride = 9 * static_cast(nqpts); + mfem::forall(nelems, [=] MFEM_HOST_DEVICE(int i_local) { + const int i_input = local2global ? local2global[i_local] : i_local; + // tr_bar is element-constant: computed once, applied at every point. + const double tr_bar = BBarTrace( + nnodes, + bbar_data + static_cast(i_local) * elem_stride, + vel_data + static_cast(i_input) * elem_stride); + double* vgrad_elem = vel_grad_data + i_local * vgrad_elem_stride; + for (int q = 0; q < nqpts; ++q) { + BBarCorrectVelocityGradient(tr_bar, vgrad_elem + 9 * q); + } + }); +} } // end namespace kernel } // end namespace exaconstit \ No newline at end of file diff --git a/src/utilities/mechanics_kernels.hpp b/src/utilities/mechanics_kernels.hpp index e7d139a..fd72a2c 100644 --- a/src/utilities/mechanics_kernels.hpp +++ b/src/utilities/mechanics_kernels.hpp @@ -4,10 +4,13 @@ #include "mfem_expt/partial_qfunc.hpp" #include "options/option_parser_v2.hpp" -#include "RAJA/RAJA.hpp" #include "mfem.hpp" #include "mfem/general/forall.hpp" +#include "RAJA/RAJA.hpp" + +#include + /** * @brief ExaConstit computational kernels for finite element operations. * @@ -541,8 +544,9 @@ double ComputeVolAvgTensorFilterFromPartial(const mfem::expt::PartialQuadratureF *ir, mfem::GeometricFactors::DETERMINANTS); // Get the local-to-global element mapping and data layout info - auto l2g = pqs->GetLocal2Global().Read(); // Maps local element index to global element index - auto loc_offsets = pqs->getOffsets().Read(); // Offsets for local data layout + auto l2g = pqs->GetLocal2Global().Read(); // Maps local element index to global element index + auto loc_offsets = + pqs->Offsets(mfem::QSpaceOffsetStorage::COMPRESSED).Read(); // Offsets for local data layout auto global_offsets = (pqs->GetGlobalOffset().Size() > 1) ? pqs->GetGlobalOffset().Read() : loc_offsets; // Offsets for global data layout @@ -762,8 +766,9 @@ double ComputeVolAvgTensorFromPartial(const mfem::expt::PartialQuadratureFunctio *ir, mfem::GeometricFactors::DETERMINANTS); // Get the local-to-global element mapping and data layout info - auto l2g = pqs->GetLocal2Global().Read(); // Maps local element index to global element index - auto loc_offsets = pqs->getOffsets().Read(); // Offsets for local data layout + auto l2g = pqs->GetLocal2Global().Read(); // Maps local element index to global element index + auto loc_offsets = + pqs->Offsets(mfem::QSpaceOffsetStorage::COMPRESSED).Read(); // Offsets for local data layout auto global_offsets = (pqs->GetGlobalOffset().Size() > 1) ? pqs->GetGlobalOffset().Read() : loc_offsets; // Offsets for global data layout @@ -893,6 +898,223 @@ double ComputeVolAvgTensorFromPartial(const mfem::expt::PartialQuadratureFunctio return total_volume; } +/** + * @name B-bar / F-bar kinematics shared by the B-bar integrator and the material models + * + * @details With the B-bar integrator (IntegrationModel::BBAR) the element residual is + * R_e = sum_q w_q det(J_q) Bbar_q^T sigma_q, where Bbar replaces the volumetric part of + * the strain-rate operator with its element average (Hughes 1980; Hughes, The Finite + * Element Method, Sec. 4.5.2): + * + * element-averaged physical shape gradients: bbar_a = (1/V) sum_q w_q det(J_q) grad N_a(x_q), + * V = sum_q w_q det(J_q) + * averaged divergence of the velocity: tr_bar = sum_a bbar_a . v_a + * = (1/V) sum_q w_q det(J_q) div v(x_q) + * B-bar velocity gradient at a point: Lbar = L + (tr_bar - tr L)/3 I + * + * Bbar applied to the element velocities gives exactly sym(Lbar) (GenerateGradBarMatrix), + * and the B-bar tangent operator already linearizes with Lbar. For the stress at each + * point to come from the same kinematics, the material models receive Lbar (ExaCMech) or + * its deformation-gradient counterpart Fbar (UMAT); the residual int Bbar^T sigma(Lbar) + * and the tangent int Bbar^T C Bbar are then a consistent pair. + * + * These helpers are the single implementation of those quantities: + * - BBarAccumulateQpt / BBarFinalize: bbar for one element. Called per element by the + * B-bar integrator's full-assembly paths, and through ElementAveragedShapeGradients + * by its partial/element-assembly setup and by the ExaCMech model. + * - BBarTrace / BBarCorrectVelocityGradient: tr_bar and Lbar (ExaCMech, through + * ApplyBBarVelocityGradient). + * - FBarElement: Fbar = (Jbar / det F)^(1/3) F with Jbar the element's volume-averaged + * det F, i.e. V_current / V_reference (UMAT). Its increment Fbar_end Fbar_beg^-1 has + * the element-constant volume ratio V_end / V_beg, the time integral of tr_bar. + * + * Layouts (column-major; the same as mfem::DenseMatrix, the reordered geometric-factor + * Jacobians used by the integrators, and GradCalc): + * - Jacobian at one point: jac[i + 3 j] = d x_i / d xi_j + * - Reference shape derivatives: dshape[a + nnodes m] = d N_a / d xi_m + * - Averaged physical gradients: bbar[a + nnodes k] = d Nbar_a / d x_k + * - Element velocities: vel[a + nnodes k] = v_k at node a + * - Velocity gradient at one point: vgrad[i + 3 j] = d v_i / d x_j + * - Deformation gradient at a point: defgrad[i + 3 j] = d x_i / d X_j + * All routines assume 3D (3x3 Jacobians). + * @{ + */ + +/** + * @brief Add one quadrature point's contribution to an element's averaged shape gradients. + * + * @param nnodes number of element nodes (dofs per component) + * @param jac Jacobian d x / d xi at the point, column-major 3x3 + * @param dshape reference shape derivatives at the point, nnodes x 3 column-major + * @param weight quadrature weight w_q (without det J) + * @param[in,out] bbar_sum running sum of w_q det(J_q) grad N_a, nnodes x 3 column-major + * @param[in,out] volume running sum of w_q det(J_q) + * + * @details Uses det(J) grad N_a = adj(J)^T-contracted reference derivatives, + * det(J) dN_a/dx_k = sum_m dN_a/dxi_m adj(J)(m, k), so no inverse is formed. + * Initialize bbar_sum and volume to zero, call once per point, then BBarFinalize. + */ +MFEM_HOST_DEVICE inline void BBarAccumulateQpt(const int nnodes, + const double* jac, + const double* dshape, + const double weight, + double* bbar_sum, + double& volume) { + const double J11 = jac[0], J21 = jac[1], J31 = jac[2]; + const double J12 = jac[3], J22 = jac[4], J32 = jac[5]; + const double J13 = jac[6], J23 = jac[7], J33 = jac[8]; + const double det_j = J11 * (J22 * J33 - J32 * J23) - J21 * (J12 * J33 - J32 * J13) + + J31 * (J12 * J23 - J22 * J13); + // adj(J)(m, k), the transposed cofactor matrix + const double adj[3][3] = { + {J22 * J33 - J23 * J32, J13 * J32 - J12 * J33, J12 * J23 - J13 * J22}, + {J23 * J31 - J21 * J33, J11 * J33 - J13 * J31, J13 * J21 - J11 * J23}, + {J21 * J32 - J22 * J31, J12 * J31 - J11 * J32, J11 * J22 - J12 * J21}}; + for (int a = 0; a < nnodes; ++a) { + const double dn0 = dshape[a]; + const double dn1 = dshape[a + nnodes]; + const double dn2 = dshape[a + 2 * nnodes]; + for (int k = 0; k < 3; ++k) { + bbar_sum[a + nnodes * k] += weight * + (dn0 * adj[0][k] + dn1 * adj[1][k] + dn2 * adj[2][k]); + } + } + volume += weight * det_j; +} + +/** + * @brief Turn the accumulated sums into element averages: bbar = bbar_sum / volume. + * @param nnodes number of element nodes + * @param volume element volume from BBarAccumulateQpt (must be positive) + * @param[in,out] bbar nnodes x 3 column-major + */ +MFEM_HOST_DEVICE inline void BBarFinalize(const int nnodes, const double volume, double* bbar) { + const double inv_volume = 1.0 / volume; + for (int i = 0; i < 3 * nnodes; ++i) { + bbar[i] *= inv_volume; + } +} + +/** + * @brief Element-averaged velocity divergence, tr_bar = sum_a bbar_a . v_a. + * @param nnodes number of element nodes + * @param bbar element-averaged physical shape gradients, nnodes x 3 column-major + * @param vel element nodal velocities, nnodes x 3 column-major + * @return tr_bar, the same for every point of the element + */ +MFEM_HOST_DEVICE inline double BBarTrace(const int nnodes, const double* bbar, const double* vel) { + double tr_bar = 0.0; + for (int i = 0; i < 3 * nnodes; ++i) { + tr_bar += bbar[i] * vel[i]; + } + return tr_bar; +} + +/** + * @brief Replace the trace of a point's velocity gradient by the element average: + * Lbar = L + (tr_bar - tr L)/3 I. + * @param tr_bar element-averaged divergence from BBarTrace + * @param[in,out] vgrad velocity gradient at the point, column-major 3x3 + * @details Only the diagonal changes, by the same amount: the deviatoric part of + * sym(L) and the spin skw(L) are unchanged. + */ +MFEM_HOST_DEVICE inline void BBarCorrectVelocityGradient(const double tr_bar, double* vgrad) { + const double shift = (tr_bar - (vgrad[0] + vgrad[4] + vgrad[8])) / 3.0; + vgrad[0] += shift; + vgrad[4] += shift; + vgrad[8] += shift; +} + +/** + * @brief Volume-averaged F-bar for one element: F_q <- (Jbar / det F_q)^(1/3) F_q with + * Jbar = sum_q wdetj0_q det F_q / sum_q wdetj0_q. + * + * @param nqpts number of quadrature points of the element + * @param stride distance in doubles between consecutive points' deformation gradients (>= 9) + * @param wdetj0 w_q det(J_q) in the reference configuration of the deformation gradients + * @param[in,out] defgrad nqpts column-major 3x3 deformation gradients, point q at q * stride + * + * @details Jbar is V_current / V_reference of the element, so every det Fbar_q equals it and + * the isochoric part F / det(F)^(1/3) of each point is unchanged. Requires det F_q > 0. + */ +MFEM_HOST_DEVICE inline void +FBarElement(const int nqpts, const int stride, const double* wdetj0, double* defgrad) { + double volume_ref = 0.0; + double volume_cur = 0.0; + for (int q = 0; q < nqpts; ++q) { + const double* f = defgrad + q * stride; + const double det_f = f[0] * (f[4] * f[8] - f[5] * f[7]) - + f[1] * (f[3] * f[8] - f[5] * f[6]) + + f[2] * (f[3] * f[7] - f[4] * f[6]); + volume_ref += wdetj0[q]; + volume_cur += wdetj0[q] * det_f; + } + const double j_bar = volume_cur / volume_ref; + for (int q = 0; q < nqpts; ++q) { + double* f = defgrad + q * stride; + const double det_f = f[0] * (f[4] * f[8] - f[5] * f[7]) - + f[1] * (f[3] * f[8] - f[5] * f[6]) + + f[2] * (f[3] * f[7] - f[4] * f[6]); + const double scale = pow(j_bar / det_f, 1.0 / 3.0); + for (int i = 0; i < 9; ++i) { + f[i] *= scale; + } + } +} + +/** + * @brief Element-averaged physical shape gradients bbar for a set of elements (B-bar). + * + * @param nqpts quadrature points per element + * @param nelems number of elements to process (local count when local2global is given) + * @param nnodes nodes per element + * @param jacobian_data Jacobians d x / d xi, layout (3, 3, nqpts, input elements), column-major: + * element e, point q at e * 9 * nqpts + 9 * q (the layout GradCalc and the integrators use) + * @param loc_grad_data reference shape derivatives, layout (nnodes, 3, nqpts): + * point q at q * 3 * nnodes + * @param weights quadrature weights w_q (nqpts values; device pointer when running on device) + * @param[out] bbar_data averaged gradients, layout (nnodes, 3, nelems): element e at e * 3 * nnodes + * @param local2global optional map from processed (local) element index to the input element + * index used for jacobian_data; nullptr: identity + * + * @details One element per thread: BBarAccumulateQpt over the element's points, then + * BBarFinalize. Used by ICExaNLFIntegrator::AssemblePA and by ExaCMechModel::ModelSetup, + * so the integrator and the material model compute bbar with the same code. + */ +void ElementAveragedShapeGradients(const int nqpts, + const int nelems, + const int nnodes, + const double* jacobian_data, + const double* loc_grad_data, + const double* weights, + double* bbar_data, + const int* const local2global = nullptr); + +/** + * @brief Apply the B-bar trace correction Lbar = L + (tr_bar - tr L)/3 I to the velocity + * gradients of a set of elements. + * + * @param nqpts quadrature points per element + * @param nelems number of elements to process (local count when local2global is given) + * @param nnodes nodes per element + * @param bbar_data averaged gradients from ElementAveragedShapeGradients, + * layout (nnodes, 3, nelems) + * @param vel_data element nodal velocities, layout (nnodes, 3, input elements): element e at + * e * 3 * nnodes (the layout GradCalc reads) + * @param[in,out] vel_grad_data velocity gradients, layout (3, 3, nqpts, nelems): element e, + * point q at e * 9 * nqpts + 9 * q (the layout GradCalc writes) + * @param local2global optional map from processed element index to the input element index + * used for vel_data; nullptr: identity + */ +void ApplyBBarVelocityGradient(const int nqpts, + const int nelems, + const int nnodes, + const double* bbar_data, + const double* vel_data, + double* vel_grad_data, + const int* const local2global = nullptr); +/** @} */ + } // namespace kernel } // namespace exaconstit #endif diff --git a/src/utilities/rotations.hpp b/src/utilities/rotations.hpp index 02d0cc9..e118248 100644 --- a/src/utilities/rotations.hpp +++ b/src/utilities/rotations.hpp @@ -1,8 +1,9 @@ #pragma once -#include "ECMech_gpu_portability.h" #include "mfem.hpp" +#include "ECMech_gpu_portability.h" + #include #include diff --git a/src/utilities/unified_logger.cpp b/src/utilities/unified_logger.cpp index 68f94ff..56b475c 100644 --- a/src/utilities/unified_logger.cpp +++ b/src/utilities/unified_logger.cpp @@ -39,20 +39,21 @@ void UnifiedLogger::initialize(const ExaOptions& options) { PostProcessingFileManager file_manager(options); // Step 1: Set up log directory path // Use file manager's structure if available for consistency - fs::path base_dir = fs::weakly_canonical(file_manager.GetOutputDirectory()); + // The output directory was already resolved once on rank 0 and broadcast to all + // ranks in ExaOptions::parse_options, so it is used as is rather than resolving any + // symlinks in it again here. + fs::path base_dir = file_manager.GetOutputDirectory(); log_directory = base_dir / "logs"; - // Step 2: Create directory structure (handling symlinks properly) + // Step 2: Create directory structure if (mpi_rank == 0) { + // No exists() check first: create_directories() is a no-op if the directory + // already exists, and checking first only opens a window for the path to change + // between the check and the creation. std::error_code ec; - - // Check if the path exists after resolving symlinks - if (!fs::exists(log_directory)) { - fs::create_directories(log_directory, ec); - if (ec) { - std::cerr << "Warning: Failed to create log directory: " << ec.message() - << std::endl; - } - } else if (!fs::is_directory(log_directory)) { + fs::create_directories(log_directory, ec); + if (ec) { + std::cerr << "Warning: Failed to create log directory: " << ec.message() << std::endl; + } else if (!fs::is_directory(log_directory, ec)) { std::cerr << "Error: Log path exists but is not a directory: " << log_directory << std::endl; } @@ -442,21 +443,21 @@ std::string UnifiedLogger::end_capture() { // Step 7: Write file ONLY if content exists if (!sv.empty() && ctx->has_content) { - // Resolve symlinks in the output path - fs::path output_path = fs::weakly_canonical(ctx->output_filename); + // The log directory is built from the output directory that was resolved once in + // ExaOptions::parse_options, so the path is used as is. Several ranks can reach + // this at the same time, so the directory is created without a separate exists() + // check beforehand (create_directories() is a no-op if it already exists). + const fs::path& output_path = ctx->output_filename; fs::path output_dir = output_path.parent_path(); - // Ensure output directory exists (with symlink handling) std::error_code ec; - if (!fs::exists(output_dir)) { - fs::create_directories(output_dir, ec); - if (ec) { - std::cerr << "Warning: Failed to create output directory: " << output_dir << ": " - << ec.message() << std::endl; - } + fs::create_directories(output_dir, ec); + if (ec) { + std::cerr << "Warning: Failed to create output directory: " << output_dir << ": " + << ec.message() << std::endl; } - // Open output file using resolved path + // Open output file std::ofstream out_file(output_path, std::ios::app); if (out_file.is_open()) { // Write header with metadata diff --git a/workflows/Stage3/pre_main_post_script/voxel_coarsen/Cargo.toml b/workflows/Stage3/pre_main_post_script/voxel_coarsen/Cargo.toml index 679f01a..051afd8 100644 --- a/workflows/Stage3/pre_main_post_script/voxel_coarsen/Cargo.toml +++ b/workflows/Stage3/pre_main_post_script/voxel_coarsen/Cargo.toml @@ -7,11 +7,11 @@ edition = "2021" [dependencies] data_reader = {git="https://github.com/rcarson3/rust_data_reader.git"} -anyhow = {version="1.0"} +anyhow = {version="1.0.104"} rand = {version = "0.8"} -polars = {version = "0.30.0", optional=true, features = ["csv"]} -numpy = {version = "0.21", optional=true} -pyo3 = { version = "0.21", optional=true, features = ["abi3-py37", "extension-module", "multiple-pymethods", "anyhow"] } +polars = {version = "0.55", optional=true, features = ["csv"]} +numpy = {version = "0.29", optional=true} +pyo3 = { version = "0.29", optional=true, features = ["abi3-py39", "extension-module", "multiple-pymethods", "anyhow"] } [features] python = ["numpy", "pyo3"] diff --git a/workflows/Stage3/pre_main_post_script/voxel_coarsen/README.md b/workflows/Stage3/pre_main_post_script/voxel_coarsen/README.md index b4ab0a7..8ced81d 100644 --- a/workflows/Stage3/pre_main_post_script/voxel_coarsen/README.md +++ b/workflows/Stage3/pre_main_post_script/voxel_coarsen/README.md @@ -1,7 +1,7 @@ # Voxel Coarsen This is a Rust crate with python bindings that performs the coarsening steps of the ExaCA data in preparation for an ExaConstit simulation. -This crate assumes the ExaCA file traverses data in the following axis order from fastest to slowest y->x->z . If this changes than the logic will need to be updated. +This crate assumes the ExaCA file traverses data in the following axis order from fastest to slowest x->y->z (the order ExaCA currently writes its output in). If this changes then the logic in `rearrange_data` will need to be updated. In order to keep compile times lowish, I make use of a data reader crate (an old personal project of mine) that functions in a similar manner to Python's loadtxt with fairly decent read / parsing speeds (150-250 MB/s on an old 2013 mac). This simple data reader is roughly 2x as slow as the Rust "Polars" crate which I have as an optional feature. I'm not using that optional feature by default as it has a compile on time on the 10s of minutes due to what appears to be the fat-LTO that LLVM is doing on everything. diff --git a/workflows/Stage3/pre_main_post_script/voxel_coarsen/src/coarsen/mod.rs b/workflows/Stage3/pre_main_post_script/voxel_coarsen/src/coarsen/mod.rs index 06de76e..a7fb98c 100644 --- a/workflows/Stage3/pre_main_post_script/voxel_coarsen/src/coarsen/mod.rs +++ b/workflows/Stage3/pre_main_post_script/voxel_coarsen/src/coarsen/mod.rs @@ -27,8 +27,14 @@ fn read_data(f: &str) -> Result>, Error> { #[cfg(feature = "polars")] fn read_data(f: &str) -> PolarsResult { - CsvReader::from_path(f)?.has_header(false).with_skip_rows(2) - .with_delimiter(b'%').with_comment_char(Some(b',')).finish() + // Same layout as the data_reader version: 2 header lines to skip, comma + // separated columns, and '%' starting a comment. + CsvReadOptions::default() + .with_has_header(false) + .with_skip_rows(2) + .map_parse_options(|opts| opts.with_separator(b',').with_comment_prefix(Some("%"))) + .try_into_reader_with_file_path(Some(f.into()))? + .finish() } fn sort_data(chunk_size: usize, data: &mut [i32]) { @@ -50,8 +56,8 @@ fn rearrange_data( assert!(chunk_size.pow(3) <= (box_size.0 * box_size.1 * box_size.2)); orig_data.iter().enumerate().for_each(|(index, val)| { - // CA results are indexed as y being fastest axis indexed on, - // then x axis, and finally the z axis. + // CA results are indexed as x being fastest axis indexed on, + // then y axis, and finally the z axis. let i = index % box_size.0; let j = (index / (box_size.0)) % box_size.1; let k = index / (box_size.0 * box_size.1); diff --git a/workflows/Stage3/pre_main_post_script/voxel_coarsen/src/pycoarsen/mod.rs b/workflows/Stage3/pre_main_post_script/voxel_coarsen/src/pycoarsen/mod.rs index 3ced67c..ec91df1 100644 --- a/workflows/Stage3/pre_main_post_script/voxel_coarsen/src/pycoarsen/mod.rs +++ b/workflows/Stage3/pre_main_post_script/voxel_coarsen/src/pycoarsen/mod.rs @@ -1,20 +1,22 @@ use numpy::PyArray1; -use pyo3::{pymodule, Bound, types::PyModule, PyResult, Python}; +use pyo3::prelude::PyModuleMethods; +use pyo3::{pyfunction, pymodule, types::PyModule, wrap_pyfunction, Bound, PyResult, Python}; use crate::coarsen::voxel_coarsen; -#[pymodule] -fn rust_voxel_coarsen(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - #[pyfn(m)] - #[pyo3(name = "voxel_coarsen")] - fn voxel_coarsen_py<'py>( - py: Python<'py>, - file: &str, - coarsen_size: usize, - ) -> anyhow::Result<((usize, usize, usize), Bound<`py, PyArray1>)> { - let result = voxel_coarsen(file, coarsen_size)?; - Ok((result.0, PyArray1::from_vec_bound(py, result.1))) - } +#[pyfunction] +#[pyo3(name = "voxel_coarsen")] +fn voxel_coarsen_py<'py>( + py: Python<'py>, + file: &str, + coarsen_size: usize, +) -> anyhow::Result<((usize, usize, usize), Bound<'py, PyArray1>)> { + let result = voxel_coarsen(file, coarsen_size)?; + Ok((result.0, PyArray1::from_vec(py, result.1))) +} +#[pymodule] +fn rust_voxel_coarsen(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(voxel_coarsen_py, m)?)?; Ok(()) -} \ No newline at end of file +}