diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 390a27f2..4dae3029 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: container: ${{ matrix.container }} env: - VCPKG_COMMIT: c3867e714dd3a51c272826eea77267876517ed99 + VCPKG_COMMIT: 72d876b20f06129a1b1e0db80c7075fe3e419d7c VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}/.vcpkg-bincache name: "πŸ§ͺ Test on ${{ matrix.os }} (βš™οΈ: ${{ matrix.config }}, πŸ’Ώ: ${{ matrix.container || matrix.os }})" diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index da204efb..e15b8958 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -30,7 +30,7 @@ jobs: container: ${{ matrix.container }} env: - VCPKG_COMMIT: c3867e714dd3a51c272826eea77267876517ed99 + VCPKG_COMMIT: 72d876b20f06129a1b1e0db80c7075fe3e419d7c VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}/.vcpkg-bincache name: "🐍 Test Bindings on ${{ matrix.os }}" @@ -126,6 +126,10 @@ jobs: name: "🐍 Package Bindings on ${{ matrix.os }}" + env: + VCPKG_COMMIT: 72d876b20f06129a1b1e0db80c7075fe3e419d7c + VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}/.vcpkg-bincache + steps: - name: πŸ“₯ Checkout uses: actions/checkout@v4 @@ -138,10 +142,40 @@ jobs: - name: πŸ›ž CIBuildWheel run: pip install cibuildwheel==3.4.0 --break-system-packages + - name: πŸ–₯️ Setup vcpkg (Windows) + shell: pwsh + if: ${{ matrix.os == 'windows-latest' }} + run: | + Remove-Item Env:VCPKG_ROOT -ErrorAction SilentlyContinue + + git clone https://github.com/microsoft/vcpkg.git + git -C vcpkg checkout $env:VCPKG_COMMIT + & .\vcpkg\bootstrap-vcpkg.bat + + "VCPKG_ROOT=$($env:GITHUB_WORKSPACE)\vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + New-Item -ItemType Directory -Force -Path "${{ env.VCPKG_DEFAULT_BINARY_CACHE }}" + + - name: πŸ¦₯ Cache vcpkg binary (Windows) + if: ${{ matrix.os == 'windows-latest' }} + uses: actions/cache@v4 + with: + path: ${{ env.VCPKG_DEFAULT_BINARY_CACHE }} + key: vcpkg-${{ matrix.os }}-Release-${{ env.VCPKG_COMMIT }} + + - name: πŸ› οΈ Build VTK (Windows) + if: ${{ matrix.os == 'windows-latest' }} + shell: pwsh + run: | + ./vcpkg/vcpkg install --triplet x64-windows + - name: πŸ› οΈ Package Config (Windows) if: ${{ matrix.os == 'windows-latest' }} + shell: pwsh run: | - sed -i 's/\(DVIENNALS_BUILD_PYTHON=ON"\)/\1,"-DVIENNALS_PACKAGE_PYTHON=ON","-DVIENNALS_IS_CI=ON"/g' pyproject.toml + $toolchain = "${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake".Replace('\', '/') + sed -i 's/\(DVIENNALS_BUILD_PYTHON=ON"\)/\1,"-DVIENNALS_IS_CI=ON"/g' pyproject.toml + "CIBW_CONFIG_SETTINGS=cmake.define.CMAKE_TOOLCHAIN_FILE=$toolchain cmake.define.VCPKG_TARGET_TRIPLET=x64-windows" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append type pyproject.toml - name: πŸ› οΈ Package Config (MacOs) diff --git a/CMakeLists.txt b/CMakeLists.txt index fdcc0204..56f3a8a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.20 FATAL_ERROR) project( ViennaLS LANGUAGES CXX - VERSION 5.7.3) + VERSION 5.8.0) # -------------------------------------------------------------------------------------------------------- # Library options @@ -11,6 +11,8 @@ project( option(VIENNALS_USE_VTK "Build with VTK file support" ON) option(VIENNALS_VTK_RENDERING "Build with VTK rendering support" ON) +option(VIENNALS_USE_GPU "Enable GPU-accelerated BiCGSTAB linear solver via CUDA" OFF) + option(VIENNALS_PRECOMPILE_HEADERS "Build template specialisations for shorter compile times" OFF) option(VIENNALS_STATIC_BUILD "Build dependencies as static libraries" OFF) option(VIENNALS_ENABLE_SANITIZER "Enable Sanitizers" OFF) @@ -90,6 +92,72 @@ if(MSVC) target_compile_definitions(${PROJECT_NAME} INTERFACE _USE_MATH_DEFINES=1) endif() +# -------------------------------------------------------------------------------------------------------- +# GPU support (CUDA Runtime, no OptiX) +# -------------------------------------------------------------------------------------------------------- + +if(VIENNALS_USE_GPU) + enable_language(CUDA) + get_filename_component(_VIENNALS_CUDA_COMPILER_REAL "${CMAKE_CUDA_COMPILER}" REALPATH) + get_filename_component(_VIENNALS_CUDA_COMPILER_BIN "${_VIENNALS_CUDA_COMPILER_REAL}" DIRECTORY) + get_filename_component(_VIENNALS_CUDA_COMPILER_ROOT "${_VIENNALS_CUDA_COMPILER_BIN}" DIRECTORY) + if(CMAKE_CUDA_COMPILER AND NOT DEFINED CUDAToolkit_ROOT) + set(CUDAToolkit_ROOT + "${_VIENNALS_CUDA_COMPILER_ROOT}" + CACHE PATH "CUDA Toolkit root used by ViennaLS") + endif() + find_package(CUDAToolkit REQUIRED) + + if(CUDAToolkit_NVCC_EXECUTABLE) + get_filename_component(_VIENNALS_CUDATOOLKIT_NVCC_REAL "${CUDAToolkit_NVCC_EXECUTABLE}" + REALPATH) + if(NOT _VIENNALS_CUDATOOLKIT_NVCC_REAL STREQUAL _VIENNALS_CUDA_COMPILER_REAL) + message( + FATAL_ERROR + "[ViennaLS] CUDA compiler/toolkit mismatch:\n" + " CMAKE_CUDA_COMPILER=${CMAKE_CUDA_COMPILER}\n" + " CUDAToolkit_NVCC_EXECUTABLE=${CUDAToolkit_NVCC_EXECUTABLE}\n" + "Use a fresh build directory or pass -DCUDAToolkit_ROOT=${_VIENNALS_CUDA_COMPILER_ROOT} " + "so CUDAToolkit matches the selected CUDA compiler.") + endif() + endif() + + # Compile the CUDA kernels into a small static library so that consuming + # .cpp files (compiled by g++) never see __global__ / <<< >>> syntax. + add_library(ViennaLS_GPU STATIC ${PROJECT_SOURCE_DIR}/gpu/lsOxidationBiCGSTABKernels.cu) + + target_compile_definitions(ViennaLS_GPU PRIVATE VIENNALS_GPU_BICGSTAB=1) + + target_include_directories(ViennaLS_GPU PRIVATE ${PROJECT_SOURCE_DIR}/include/viennals + ${CUDAToolkit_INCLUDE_DIRS}) + + set_target_properties(ViennaLS_GPU PROPERTIES CUDA_SEPARABLE_COMPILATION OFF + POSITION_INDEPENDENT_CODE ON) + + target_link_libraries(ViennaLS_GPU PUBLIC CUDA::cudart CUDA::cusparse) + + if(UNIX AND NOT APPLE) + foreach(_VIENNALS_CUDA_TARGET CUDA::cudart CUDA::cusparse) + get_target_property(_VIENNALS_CUDA_LIBRARY ${_VIENNALS_CUDA_TARGET} IMPORTED_LOCATION) + if(_VIENNALS_CUDA_LIBRARY) + get_filename_component(_VIENNALS_CUDA_LIBRARY_DIR "${_VIENNALS_CUDA_LIBRARY}" DIRECTORY) + target_link_options(ViennaLS_GPU INTERFACE + "$") + endif() + endforeach() + endif() + + # Expose the define and the GPU library to downstream targets. + # BUILD_INTERFACE limits the ViennaLS_GPU dependency to the build tree so it + # is never exported to the install tree (avoiding the "not in export set" error + # from packageProject when ViennaLS_GPU is a non-installed local target). + target_compile_definitions(${PROJECT_NAME} INTERFACE VIENNALS_GPU_BICGSTAB=1) + target_link_libraries(${PROJECT_NAME} INTERFACE $) + target_include_directories(${PROJECT_NAME} INTERFACE ${CUDAToolkit_INCLUDE_DIRS}) + + message(STATUS "[ViennaLS] GPU BiCGSTAB solver enabled (CUDA ${CUDAToolkit_VERSION})") +endif() + # -------------------------------------------------------------------------------------------------------- # Include directories # -------------------------------------------------------------------------------------------------------- @@ -128,6 +196,9 @@ CPMFindPackage( GIT_REPOSITORY "https://github.com/ViennaTools/ViennaHRLE" EXCLUDE_FROM_ALL ${VIENNALS_BUILD_PYTHON}) +# # Use local ViennaHRLE with interior point support +# add_subdirectory(../ViennaHRLE ViennaHRLE.build) + target_link_libraries(${PROJECT_NAME} INTERFACE ViennaHRLE ViennaCore) if(VIENNALS_USE_VTK AND VIENNALS_VTK_PYTHON_LIBS) diff --git a/README.md b/README.md index f47232e7..6e167460 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,15 @@ -ViennaLS is a header-only C++ level set library developed for high performance topography simulations. The main design goals are simplicity and efficiency, tailored towards scientific simulations. ViennaLS can also be used for visualisation applications, although this is not the main design target. +ViennaLS is a header-only C++ level set library for high-performance topography and semiconductor process simulations. The main design goals are efficiency and simplicity, tailored towards scientific process simulation. + +## Capabilities + +- **Level-set equation solver**: Solves βˆ‚Ο†/βˆ‚t + v|βˆ‡Ο†| = 0 to advect interfaces under arbitrary velocity fields; spatial schemes: Engquist-Osher (1st/2nd order), Lax-Friedrichs (global/local, 1st/2nd order), WENO (3rd/5th order); temporal schemes: Forward Euler, Runge-Kutta 2nd/3rd order; CFL-limited time stepping +- **Level-set operations**: Boolean operations, geometric advection, curvature calculation, normal vectors calculation, visibility calculation, feature detection, 2D<->3D extrusion/slicing, mesh export (VTP/VTU via VTK) +- **Comparison & analysis**: Chamfer distance (bidirectional surface-to-surface, forward/backward/RMS/max), narrow-band SDF difference, sparse-field SDF difference, volume/area difference, critical dimension extraction +- **Thermal oxidation**: Coupled Deal-Grove diffusion + Stokes flow oxide mechanics (Maxwell viscoelasticity, SIMPLE pressure-velocity coupling); LOCOS mode adds biharmonic Si₃Nβ‚„ mask bending with two-way traction coupling. See [`docs/OxidationSolver.md`](docs/OxidationSolver.md) +- **GPU acceleration**: Optional CUDA BiCGSTAB back-end for the oxidation diffusion and mechanics linear solves (requires `VIENNALS_USE_GPU=ON`) > [!NOTE] > ViennaLS is under heavy development and improved daily. If you do have suggestions or find bugs, please let us know! @@ -32,7 +40,7 @@ Bug reports and suggestions should be filed on GitHub. ## Releases -Releases are tagged on the maser branch and available in the [releases section](https://github.com/ViennaTools/ViennaLS/releases). +Releases are tagged on the master branch and available in the [releases section](https://github.com/ViennaTools/ViennaLS/releases). ## Building @@ -107,6 +115,19 @@ cd ViennaLS pip install . ``` +To build the Python package with **GPU** support for the oxidation diffusion and mechanics solvers, use the install script in the `python/scripts` folder: + +```bash +git clone https://github.com/ViennaTools/ViennaLS.git +cd ViennaLS + +python3 -m venv .venv # create virtual environment (optional, but recommended) +source .venv/bin/activate # activate virtual environment +python python/scripts/install_ViennaLS.py +``` + +A CUDA 12+ toolkit and a driver compatible with your GPU must be installed on your system. On Linux, a GCC version compatible with the CUDA toolkit is also required. Pass `--no-gpu` to build without GPU support using the same script and venv workflow. + ## Using the Python package The ViennaLS Python package can be used by importing it in your Python scripts: @@ -154,7 +175,7 @@ We recommend using [CPM.cmake](https://github.com/cpm-cmake/CPM.cmake) to consum * Installation with CPM ```cmake - CPMAddPackage("gh:viennatools/viennals@5.7.3") + CPMAddPackage("gh:viennatools/viennals@5.8.0") ``` * With a local installation diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 9efa8e74..c50e5c9a 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -35,5 +35,3 @@ This file lists third-party libraries used by ViennaLS and their respective lice **Note:** Used only when building Python bindings. --- - - diff --git a/cmake/cpm.cmake b/cmake/cpm.cmake index bab79bbe..bbbdcd6f 100644 --- a/cmake/cpm.cmake +++ b/cmake/cpm.cmake @@ -16,8 +16,10 @@ endif() # Expand relative path. This is important if the provided path contains a tilde (~) get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) -file(DOWNLOAD - https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake - ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM}) +if(NOT EXISTS ${CPM_DOWNLOAD_LOCATION}) + file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM}) +endif() include(${CPM_DOWNLOAD_LOCATION}) diff --git a/docs/OxidationSolver.md b/docs/OxidationSolver.md new file mode 100644 index 00000000..ca6baeb7 --- /dev/null +++ b/docs/OxidationSolver.md @@ -0,0 +1,567 @@ +# Thermal Oxidation Solver + +This document describes the physics model, numerical algorithms, and implementation +of the coupled oxidation solver in ViennaLS/ViennaPS. The solver computes thermal +silicon oxidation by solving a coupled system of a diffusion PDE, a Stokes-flow +mechanical deformation PDE, and (in LOCOS mode) a mask bending PDE, all on a shared +Cartesian grid extracted from the ViennaLS level-set narrow bands. + +--- + +## 1. Physical Model: Deal-Grove Oxidation + +Silicon oxidation follows the Deal-Grove linear-parabolic model (B. E. Deal and A. S. +Grove, *J. Appl. Phys.* **36**, 3770, 1965). Three sequential steps limit the overall +rate: + +1. **Oxidant transport** from the gas phase through the existing oxide to the Si/SiO2 + interface. +2. **Surface reaction** of the oxidant with silicon at the Si/SiO2 interface. +3. **Volume expansion**: consuming 1 Β΅mΒ³ of Si produces 2.27 Β΅mΒ³ of SiO2, so both + the Si/SiO2 interface recedes and the SiO2/gas (ambient) interface rises. + +The two rate constants are: +- **B** (Β΅mΒ²/hr) β€” parabolic rate constant, proportional to the oxidant + diffusivity D through the oxide: `B = 2 D C* / N`, where C* is the + equilibrium oxidant concentration at the ambient surface and N is the + oxidant molecule density in the oxide. +- **B/A** (Β΅m/hr) β€” linear rate constant, equal to the surface reaction rate k_s + scaled by the concentration ratio: `B/A = k_s C* / N`. + +Both constants are thermally activated (Arrhenius): + +``` +B = Bβ‚€ Β· exp(βˆ’E_B / k_B T) +B/A = BoAβ‚€ Β· exp(βˆ’E_BoA / k_B T) +``` + +The temperature-dependent Arrhenius table is implemented in `psOxidation.hpp` +(`dealGroveRow()`). Separate regimes apply for dry oxidation above and below 950 Β°C +(Massoud, Plummer & Irene, *J. Electrochem. Soc.* **132**, 2685, 1985). + +### Orientation Dependence + +The linear rate B/A depends on the exposed crystal face. The solver applies a +continuous correction per reaction-boundary face: + +``` +k(nΜ‚) = k_base Β· [1 + (r βˆ’ 1) Β· (1 βˆ’ (nΜ‚ Β· Γ’)Β²)] +``` + +where nΜ‚ is the outward normal at the Si/SiO2 crossing, Γ’ is the wafer-normal +crystal axis (Y by convention), and r = `reactionRateRatio111` is the ratio +of the perpendicular-face rate to the bulk-orientation rate. For Si(100): r = 1.45 +(perpendicular faces are (110)-like); for Si(111): r = 1/1.68; for Si(110): r = 1/1.45. +This captures the Bird's-Beak anisotropy observed in LOCOS without requiring explicit +crystal-plane tracking. + +### Stress-Dependent Kinetics + +Compressive stress in the growing oxide modifies both the reaction rate and the +diffusivity via an activation-volume coupling (Kao et al., 1987): + +``` +k_eff(p) = k Β· exp(βˆ’(p βˆ’ p_ref) Β· V_k / (k_B T)) +D_eff(p) = D Β· exp(βˆ’(p βˆ’ p_ref) Β· V_D / (k_B T)) +``` + +where p is the local hydrostatic pressure in Pa, V_k and V_D are the stress-coupling +activation volumes (mΒ³), and p_ref is the reference pressure (zero by default). With +the default `V_k = 1.76Γ—10⁻³⁡ mΒ³`, compressive pressures of ~1 GPa reduce the +interface reaction rate by ~20% at 1000 Β°C, self-limiting the oxide growth in +confined geometries. + +--- + +## 2. Level-Set Representation + +The solver tracks two (or three) signed-distance level-set functions: + +| Level set | Sign convention | Physical meaning | +|---|---|---| +| `reactionInterface` (Si) | positive = solid Si | Si/SiO2 boundary | +| `ambientInterface` (SiO2) | positive = outside oxide | SiO2/gas boundary | +| `maskInterface` (Si3N4) | positive = outside mask | Mask/oxide boundary (LOCOS only) | + +A point is inside the oxide when: +- `reactionSign Β· Ο†_reaction β‰₯ 0` (on the Si side of the Si/SiO2 interface or + beyond it into the oxide), **and** +- `ambientSign Β· Ο†_ambient β‰₯ 0` (below the ambient surface). + +ViennaLS uses HRLE (hierarchical run-length encoding) to represent the narrow bands +efficiently. Both level sets are iterated simultaneously to find oxide-interior +grid points. + +### Floating-Point Robustness + +`GeometricAdvect` (used to seed the initial thin oxide) computes interface positions +from `k * gridDelta` arithmetic. For non-zero k this is not exactly representable in +IEEE 754 double, so the ambient surface can land at `Ο† = +8.87Γ—10⁻¹⁢` instead of +exactly 0.0. Without a tolerance, `isInsideOxide` and `crosses()` both fail silently: +no oxide nodes are created at the ambient surface row, no AMBIENT boundary condition +is imposed, and the diffusion field gets no source β€” leaving zero oxide growth. + +The fix (eps = 1Γ—10⁻⁹) in `isInsideOxide()` and `crosses()` handles this across the +board: it is large enough to absorb the ~4Γ—DBL_EPSILON residual from `GeometricAdvect` +and small enough to never merge two grid-cell-separated surfaces (gridDelta β‰₯ 0.001 Β΅m +in practice). + +--- + +## 3. Cartesian Solve Grid + +Before each time step the solver builds a structured Cartesian grid over the oxide +region. The grid is extracted from the union of the HRLE narrow bands: + +1. **Scan defined points** in both level sets; record the index bounding box with + Β±4-cell padding (`definedPointBounds` in `OxidationSolverBase`). +2. **Allocate a flat look-up array** (`nodeLookupFlat`) of size + `(maxI - minI + 1) Γ— (maxJ - minJ + 1)` (times `(maxK - minK + 1)` in 3D) + initialised to `noNode`. +3. **Walk the bounding box**; for each grid index call `isInsideOxide(Ο†_reaction, Ο†_ambient)`. + Every point that passes is registered as a solve node and its linear index is written + into `nodeLookupFlat`. Faces between two oxide nodes are skipped; only faces that + cross a level-set surface get classified as boundaries. + +The maximum node count is guarded by `maxGridPoints` (default 5 million). The same +index range is used by both the diffusion and deformation solvers, giving them an +identical stencil neighbourhood and simplifying pressure ↔ concentration coupling. + +--- + +## 4. Sub-Grid Boundary Geometry + +Finite-difference operators at boundary faces need the distance from the node centre +to the physical interface, not just whether a crossing exists. The solver uses linear +interpolation of the signed-distance values: + +``` +d = gridDelta Β· |Ο†_inside| / (|Ο†_inside| + |Ο†_outside|) +``` + +clamped to `[minBoundaryDistance Γ— gridDelta, gridDelta]`. The minimum distance guard +(default 1Γ—10⁻⁢ Β· gridDelta) prevents division-by-zero when a surface coincides with +a grid node; the gridDelta upper cap prevents negative distances. + +This sub-grid position enters every boundary coefficient: + +- **AMBIENT face** (Robin BC): the transfer coefficient term `hΒ·C*Β·(Ξ”x/d)` and the + diffusion-over-distance term `DΒ·(Ξ”x/d)` use d directly, so the Robin stiffness + scales correctly with the actual oxidant path length regardless of where the surface + falls within the grid cell. +- **REACTION face** (Neumann/Robin BC): the local reaction rate `k_eff(p)` is + multiplied by the surface concentration C at the sub-grid crossing, and the + resulting flux `k_eff Β· C / N` drives the Si surface velocity. +- **MASK face** (LOCOS): the traction BC requires the oxide velocity at the actual + interface position; the deformation solver interpolates linearly using d. + +--- + +## 5. Diffusion Solver (`lsOxidationDiffusion.hpp`) + +The steady-state diffusion equation (oxidant transport is fast compared to interface +motion) is solved on the Cartesian oxide grid: + +``` +βˆ‡ Β· (D_eff(p) βˆ‡C) = 0 inside oxide +``` + +with boundary conditions at every face that crosses a level-set surface: + +| Boundary type | Condition | +|---|---| +| **AMBIENT** (SiO2/gas) | Robin (mixed): `βˆ’D βˆ‚C/βˆ‚n = h(C βˆ’ C*)` where h = transfer coefficient | +| **REACTION** (Si/SiO2) | Robin: `βˆ’D βˆ‚C/βˆ‚n = k_eff(p) Β· C / N` | +| **MASK** (LOCOS) | Robin or Dirichlet: `C = C_mask` (small non-zero value) | + +In the limit h β†’ ∞ the ambient BC becomes Dirichlet C = C* (instantaneous gas-phase +replenishment), which is the classical Deal-Grove assumption. The default transfer +coefficient of 100 Β΅m/hr approximates this well. + +The linear system is assembled face-by-face as a sparse matrix in CSR format and solved +with **BiCGSTAB** with Jacobi preconditioning. The default back-end is CPU. An optional +GPU back-end (`lsOxidationBiCGSTABInterface.hpp`) offloads the solve to CUDA when +`GpuMode::Gpu` is selected via `setGpuMode()` and a CUDA device is available. + +The Si surface velocity driven by diffusion is: + +``` +v_Si = βˆ’k_eff(p) Β· C(x_Si) / (N Β· ρ) +``` + +where C(x_Si) is the concentration at the sub-grid reaction-surface crossing and +ρ normalises by the atomic density. The expansion coefficient Ξ² = 2.27 splits this +into the Si recession rate (inward, 1/Ξ²) and the ambient growth rate (outward, +(Ξ²βˆ’1)/Ξ²). + +--- + +## 6. Deformation Solver (`lsOxidationDeformation.hpp`) + +The oxide grows as a viscous incompressible fluid driven by the volume source at the +Si/SiO2 interface. The deformation solver finds the oxide velocity field that +is consistent with both the source and the mechanics. The governing equations are the +Stokes flow problem with an incompressibility constraint modified by the expansion: + +``` +βˆ’βˆ‡p + Ξ· βˆ‡Β²u = 0 (momentum) +βˆ‡ Β· u = αΉ‘ (modified continuity: local volume source αΉ‘ at reaction boundary) +``` + +supplemented by viscoelastic stress tracking: + +``` +Οƒ = Οƒ_elastic + Οƒ_viscous + = G Β· Ξ΅_dev + Ξ· Β· Ξ΅Μ‡_dev + (K βˆ’ 2G/3) Β· tr(Ξ΅) Β· I +``` + +where G = shear modulus (~3Γ—10¹⁰ Pa), K = bulk modulus, and Ξ· = viscosity +(~10¹⁰ PaΒ·hr at 1000 Β°C, Arrhenius-scaled). The stress relaxes over a +characteristic time Ο„ = Ξ·/G. + +### 6.1 Stress Tensor Storage Convention + +Throughout the solver `nodes[i].stressTensor` stores the **Cauchy stress** Οƒ defined as: + +``` +Οƒ_{rc} = Ο„_{rc} βˆ’ Ξ΄_{rc} Β· p +``` + +where Ο„ is the true deviatoric (traceless) stress and p = `nodes[i].pressure` is the +isotropic pressure. This means `stressTensor` is **not** purely deviatoric; the +diagonal entries include βˆ’p. + +To recover the true deviatoric Ο„ for traction computations: +``` +Ο„_{rc} = stressTensor[r][c] + Ξ΄_{rc} Β· pressure (deviatoricStressAt()) +``` + +This convention is important for boundary conditions: the free-surface (AMBIENT) ghost +velocity uses `pΒ·n_comp βˆ’ Ο„_compΒ·n`, where τ·n = Ξ£_j Οƒ_{comp,j}Β·n_j + pΒ·n_comp. + +### 6.2 Maxwell Relaxation + +At each SIMPLE iteration the deviatoric stress Ο„ is updated with Maxwell relaxation: + +``` +Ο„^{new}_{rc} = e^{βˆ’Ξ”t/Ο„_r} Β· Ο„^{prev}_{rc} + (1 βˆ’ e^{βˆ’Ξ”t/Ο„_r}) Β· 2Ξ· Β· Ξ΅Μ‡^{dev}_{rc} +``` + +where Ο„_r = Ξ·/G is the stress relaxation time and Ξ΅Μ‡^{dev} is the deviatoric part of +the strain rate tensor (traceless, symmetric). + +The history is stored in `deviatoricStressHistory` (an `unordered_map>`), updated **every SIMPLE iteration** so the history reflects the most +recent velocity field, not just the beginning of each CFL step. + +### 6.3 SIMPLE Algorithm + +The incompressible Stokes system is solved with the **SIMPLE** (Semi-Implicit Method +for Pressure-Linked Equations) pressure-velocity coupling. The algorithm is +unconditionally stable for steady Stokes and avoids the spectral-radius > 1 problem +that makes a naive Gauss-Seidel pβ†’vβ†’p iteration diverge on thin geometries. + +The three steps per iteration are: + +``` +1. Momentum predictor: A_v Β· v* = vBC βˆ’ (βˆ‡p^k βˆ’ βˆ‡Β·Ο„(v^k)) / Ξ· +2. Pressure update: A_p Β· p^{k+1} = pBC(Ο„(v*)) + K Β· βˆ‡Β·v* +3. Velocity correction: v^{k+1} = v* βˆ’ βˆ‡(p^{k+1}βˆ’p^k) / (Ξ· Β· diag(A_v)[i]) +``` + +Step 3 ensures the corrected velocity is consistent with the new pressure without +re-solving the full momentum equation. + +**Convergence criterion** (both CPU and GPU): +``` +residual = max(|Ξ”v|_max / |v|_max, |Ξ”p|_max / |p|_max) < mechanicsTolerance (1e-2) +``` +Up to `mechanicsIterations` (200) outer iterations are allowed. + +**Inner solver tolerances**: `stokesTolerance=1e-3`, `pressureTolerance=1e-3`. +The hierarchy `mechanicsTolerance β‰₯ 5 Γ— max(stokesTolerance, pressureTolerance)` +must hold β€” the outer residual cannot go below the inner solver noise floor. + +**Pressure relaxation**: the solved p^{k+1} is blended with the previous pressure +before applying the velocity correction: +``` +p^{k+1}_relaxed = (1 βˆ’ Ξ²) Β· p^k + Ξ² Β· p^{k+1}_solved, Ξ² = pressureRelaxation (0.5) +``` + +**Velocity correction diagonal**: two distinct diagonal arrays are maintained: +- `stokesDiag` / `d_diagV` β€” diagonal of the Stokes operator A_v, computed from + REACTION+MASK+interior faces only (OOB and AMBIENT contributions cancel in the + matvec). Used as the preconditioner diagonal in the Stokes BiCGSTAB solve. +- `corrDiag` / `d_diagV_corr` β€” full diagonal including OOB and AMBIENT face terms, + returned by `computeVelocityDiagonals()`. Used exclusively for the velocity + correction `v^{k+1} = v* βˆ’ βˆ‡Ξ΄p / (Ξ· Β· corrDiag_i)`. + +Using `corrDiag` for the correction (instead of `stokesDiag`) is critical: at +ambient-adjacent nodes, `corrDiag > stokesDiag`, producing a smaller correction +magnitude that avoids SIMPLE oscillation. + +### 6.4 CPU SIMPLE Loop + +The CPU implementation lives in `solveMechanics()` β†’ CPU branch +(lines ~1764–1847 of `lsOxidationDeformation.hpp`). + +**Pre-loop setup** (once per `solveMechanics()` call): +``` +diagV = computeVelocityDiagonals() // corrDiag, geometry-fixed for all iterations +``` + +**Per-iteration** (up to `mechanicsIterations`): +``` +previousVelocity = collectVelocities() // save v^k +previousPressure = collectPressures() // save p^k +computeDiagnostics() // div(v^k), vonMises β€” uses OLD stress +computeStressTensors() // Maxwell update: stressTensor = Ο„βˆ’pΒ·I, history updated +solveStokesVelocity() // v* in nodes[i].velocity; uses p^k, Ο„(v^k) +solvePressure() // p^{k+1}; ambient BC uses Ο„ freshly from v* +applySimpleVelocityCorrection(p^k, diagV) // v^{k+1} = v* βˆ’ βˆ‡Ξ΄p / (Ξ·Β·corrDiag) +residual = max(maxVelChange, maxPresChange) +``` + +**After convergence** (or max iterations): +``` +computeDiagnostics() // refresh div(v), vonMises with final velocity +computeStressTensors() // refresh stressTensor with final velocity +``` + +**Key detail β€” ambient BC timing**: `solvePressure()` calls +`freeSurfacePressureBoundary()` which calls `currentBoundaryDeviatoricStress()`, +computing Ο„ freshly from the current (post-Stokes) velocity v*. This means the +ambient pressure BC for the pressure RHS uses Ο„(v*), not Ο„(v^k). This distinction +matters for correctness and is reproduced by the GPU's step 6b. + +**Ghost velocity at AMBIENT faces** (`freeSurfaceVelocityBoundary()`): uses +`deviatoricStressAt()` which recovers the true deviatoric Ο„ by adding back pΒ·Ξ΄ to +the stored Οƒ. The normalTraction formula is: +``` +normalTraction[comp] = p Β· n[comp] βˆ’ Ο„_comp Β· nΜ‚ + = p Β· n[comp] βˆ’ Ξ£_j Ο„_{comp,j} Β· n_j +``` + +### 6.5 GPU Acceleration of Linear Solves + +GPU support is conditionally compiled under `VIENNALS_GPU_BICGSTAB`. When +`GpuMode::Gpu` is set the three inner BiCGSTAB linear solves within the SIMPLE +iteration are offloaded to CUDA; the SIMPLE iteration loop itself and all RHS assembly +always run on the CPU. + +**Per-SIMPLE-iteration sequence** (CPU drives the loop): +``` +CPU: save v^k, p^k +CPU: computeDiagnostics(), computeStressTensors() β€” stress/strain update +CPU: freeSurfaceVelocityBoundary() β€” AMBIENT ghost velocity from current stress +CPU: assemble Stokes RHS b_stokes[D*n] +GPU: Stokes BiCGSTAB (gpuStokesBufs_) β€” solves A_v Β· v* = b_stokes, per component +CPU: download v* from GPU +CPU: freeSurfacePressureBoundary() β€” pressure RHS ambient BC from Ο„(v*) +CPU: assemble pressure RHS b_press[n] +GPU: Pressure BiCGSTAB (gpuPressBufs_) β€” solves A_p Β· p^{k+1} = b_press +CPU: download p^{k+1}; apply pressure relaxation; apply velocity correction +CPU: check convergence residual +``` + +The geometry-fixed coefficient arrays (`stokesCoeffGpu_`, `pressCoeffGpu_`, +`harmonicCoeffGpu_`) and neighbor ID arrays (`stokesNeighId32_`, `pressNeighId32_`) +are uploaded once per `buildNodes()` call; the RHS (`b_stokes`, `b_press`) is +re-uploaded and the solution is downloaded on every SIMPLE iteration. + +**GPU handles allocated per `buildNodes()` call** (when `GpuMode::Gpu`): + +| Handle | Purpose | +|---|---| +| `gpuPressBufs_` | BiCGSTAB buffers for the pressure Poisson solve | +| `gpuStokesBufs_` | BiCGSTAB buffers for the Stokes velocity solve (per component) | +| `gpuHarmonicBufs_` | BiCGSTAB buffers for the harmonic extension solve | + +**Activation**: +``` +tryGpu = (gpuMode_ == GpuMode::Gpu) // default is GpuMode::Cpu + +all three handles allocated when: tryGpu +``` + +**Preconditioner options** (`GpuPreconditioner`): +- `Jacobi` (default) β€” diagonal scaling +- `ILU0` β€” incomplete LU(0) via cuSPARSE; better convergence for ill-conditioned + systems at the cost of a one-time symbolic+numeric factorization per `buildNodes()` + +--- + +## 7. Pressure–Concentration Coupling + +The diffusion coefficient D_eff and reaction rate k_eff both depend on the local +oxide pressure p (from the deformation solver), while the velocity field depends on +the concentration gradient (through the volume-source boundary condition). The +coupling loop in `OxidationModel` iterates: + +``` +For i = 1, 2, ..., maxIterations: + 1. Solve diffusion with current {p_i} β†’ get C_i + 2. Solve deformation with current {C_i} β†’ get p_i+1 + 3. Compute Aitken Δ² blended pressure pΜƒ_i+1 + 4. Feed pΜƒ_i+1 back into diffusion as new pressure field + 5. Check relative change β€–pΜƒ_i+1 βˆ’ pΜƒ_iβ€– / β€–pΜƒ_i+1β€– < tolerance +``` + +**Aitken Δ² acceleration** computes the mixing parameter ΞΈ ∈ [0.1, 1.0] that +minimises the squared residual norm at each coupling iteration, updating it as +`ΞΈ = βˆ’βŸ¨F_{k-1}, Ξ”F⟩ / β€–Ξ”Fβ€–Β²`. Clamping ΞΈ ≀ 1 disables extrapolation: the +exponential dependence of rate constants on pressure makes over-shooting unstable. +The coupling typically converges in 3–5 iterations for standard geometries. + +--- + +## 8. CFL-Limited Time Stepping + +Interface advection with explicit Euler or Runge-Kutta requires the CFL condition: + +``` +Ξ”t ≀ Ξ± Γ— gridDelta / max_velocity +``` + +where Ξ± < 0.5 (default 0.499). The time-step controller in `psOxidation.hpp` and +`lsOxidation.hpp` implements a two-pass predictor-corrector: + +1. **Predictor**: solve fields at the trial Ξ”t (first step: estimated from B/A). +2. **Compute max velocity** across all interface points. +3. **CFL check**: if `v_max Γ— Ξ”t / gridDelta > Ξ±`, set `Ξ”t_actual = Ξ± Γ— gridDelta / v_max` + and re-solve fields at the smaller step. +4. **Accept** if the corrected velocity is consistent (i.e. re-checking with the + newly computed v_max still satisfies CFL). + +The outer loop in `psOxidation.hpp` accumulates steps until the total time reaches +the requested oxidation time, growing each step up to `maxStepGrowth = 2Γ—` the +previous accepted step to avoid oscillations. If a solve fails to converge, the step +is halved (up to 16 retries) before throwing. + +--- + +## 9. Interior Fill and Warm-Start Persistence + +After each advection step ViennaLS's `lsAdvect` leaves only a narrow band around the +new surface position; interior data (concentration, pressure, stress) are discarded. +The solver restores them in two phases: + +1. **Before advection** (`diffusionField->writePersistentFields()`): write solved + concentration, pressure, and viscoelastic stress tensors back into the oxide + level-set's `pointData` arrays as named scalar/vector fields (`OxConcentration`, + `OxPressure`, `OxStressR0..R2`). +2. **After advection + interior fill** (`lsInterior`): + a. `lsInterior` floods the interior of the advected level-set with the interior + value (βˆ’1 for SiO2), expanding the HRLE back to the full oxide domain. + b. `writePersistentFields()` is called again β€” now over the full interior HRLE β€” + so the next step's `buildNodes()` finds saved values at every interior node and + can warm-start the BiCGSTAB solve with the previous solution instead of zero. + +This warm-start consistently halves the BiCGSTAB iteration count on subsequent steps. + +**Important**: `lsInterior::apply()` intentionally does **not** call +`getDomain().segment()` at the end. Calling `segment()` on the freshly filled HRLE +invalidates the iterator state used by the downstream solvers and triggers a +multi-segment-domain bug in multi-threaded builds. The single-segment domain left by +`lsInterior` works correctly with all downstream iterators. + +--- + +## 10. LOCOS Mode + +When a Si3N4 mask level set is present (`maskInterface`), the solver switches to LOCOS +(Local Oxidation of Silicon) mode. LOCOS has three additional physics components: + +### 10.1 Mask Bending Solver (`lsOxidationMask.hpp`) + +The nitride mask deforms under the traction exerted by the growing oxide. The mask +is treated as a viscoelastic sheet; the governing equation is the 2D/3D biharmonic +(plate-bending) equation with a Neumann boundary condition at oxide-contact faces: + +``` +Οƒ_mask Β· nΜ‚ = Οƒ_oxide Β· nΜ‚ (traction continuity at mask/oxide interface) +``` + +The traction Οƒ_oxide Β· nΜ‚ is computed from the Stokes-flow stress tensor at the +mask surface and applied as a Neumann load on the mask boundary. The mask solve uses +a multigrid GMRES solver with a Gauss-Seidel (SOR) smoother. + +**Contact modes** (`contactMode`): +- **0 β€” kinematic**: mask velocity at contact faces equals the oxide velocity + (legacy Dirichlet BC; no traction computation). +- **1 β€” one-way (default)**: oxide traction drives mask; mask stiffness resists. + Unilateral contact (compression-only) is optional. +- **2 β€” elastic**: two-way: elastic equilibrium of the mask, with oxide contact + prescribing displacement v_oxide Γ— Ξ”t. The outer coupling loop feeds the + resulting mask displacement back into the next oxide solve. + +**Arrhenius creep viscosity** scales the mask stiffness with temperature: +``` +Ξ·_mask(T) = Ξ·_ref Β· exp(E_a / R Γ— (1/T βˆ’ 1/T_ref)) +``` +with E_a β‰ˆ 3.86Γ—10⁡ J/mol (~4 eV, Senez et al., *IEEE Trans. Electron Devices*, +1994) and Ξ·_ref = 5Γ—10ΒΉΒΉ PaΒ·hr at 1000 Β°C. + +### 10.2 Constrained Ambient Advection + +Under the nitride mask the oxide is prevented from growing upward; instead it flows +laterally (the Bird's Beak). `OxidationConstrainedAmbient` enforces this by: + +1. Accepting the full Stokes velocity at oxide faces away from the mask. +2. Projecting the velocity onto the mask-tangent plane at oxide faces in contact with + the mask, eliminating the normal component that would push the oxide through the + nitride. +3. Clipping the ambient level set to the mask exterior before advection + (`BooleanOperation: RELATIVE_COMPLEMENT`). + +### 10.3 Mask–Oxide Coupling Loop + +After the first coupled diffusion+deformation solve and mask bending solve, the +solver iterates the pair until the mask velocity field stabilises: + +``` +For j = 1, ..., maskCouplingIterations: + 1. Re-solve coupled diffusion + deformation + 2. Re-solve mask bending with updated oxide traction + 3. Compute residual: β€–v_mask^{j} βˆ’ v_mask^{jβˆ’1}β€– / β€–v_mask^{j}β€– + 4. Converge if residual < maskCouplingTolerance + OR mask displacement < tolerance Γ— gridDelta +``` + +An adaptive relaxation scale (`adaptiveMaskRelaxationScale`) halves the outer +relaxation when the mask/oxide fixed-point oscillates, and reverts to the minimum +time step as a last resort before freezing the mask for one step. + +--- + +## 11. Level-Set Advection + +After all field solves converge, the three interfaces are advanced using `lsAdvect`: + +- **Ambient interface**: advected with the deformation velocity field (Stokes + velocity at the SiO2/gas surface). In LOCOS mode: `OxidationConstrainedAmbient` + is used instead. +- **Si/SiO2 interface**: advected with the diffusion velocity field (proportional + to the local flux at the Si surface). +- **Mask interface** (LOCOS): advected with the mask bending velocity. + +The default integration scheme is first-order Engquist-Osher for spatial +derivatives, forward Euler for time (controllable via `setSpatialScheme` / +`setTemporalScheme`). + +--- + +## 12. File Map + +| File | Role | +|---|---| +| `lsOxidationSolverBase.hpp` | Cartesian grid: node lookup, bounds, `crosses()`, `isInsideOxide()` | +| `lsOxidationDiffusion.hpp` | Diffusion PDE, BiCGSTAB solve, Si surface velocity | +| `lsOxidationDeformation.hpp` | Stokes flow, SIMPLE pressure-velocity coupling, oxide velocity; CPU + GPU dispatch | +| `lsOxidationBiCGSTABInterface.hpp` | C++ (g++) interface to GPU BiCGSTAB; safe for .cpp inclusion | +| `lsOxidationBiCGSTAB.cuh` | GPU BiCGSTAB kernels (`solveBiCGSTABInPlace`); nvcc-only | +| `lsOxidationMask.hpp` | Mask bending, multigrid, contact mechanics, traction BC | +| `lsOxidationModel.hpp` | Pressure–concentration coupling loop (Aitken Δ²) | +| `lsOxidation.hpp` | Per-step orchestrator: CFL stepping, advection, interior fill | +| `lsOxidationPresets.hpp` | Named parameter presets (wet/dry at 1000 Β°C, SiN mask) | +| `psOxidation.hpp` (ViennaPS) | High-level process model: Arrhenius table, domain management | diff --git a/examples/LOCOSOxidation/CMakeLists.txt b/examples/LOCOSOxidation/CMakeLists.txt new file mode 100644 index 00000000..8ea0de85 --- /dev/null +++ b/examples/LOCOSOxidation/CMakeLists.txt @@ -0,0 +1,6 @@ +project(LOCOSOxidation LANGUAGES CXX) + +add_executable(${PROJECT_NAME} "${PROJECT_NAME}.cpp") +target_link_libraries(${PROJECT_NAME} PRIVATE ViennaLS) + +add_dependencies(ViennaLS_Examples ${PROJECT_NAME}) diff --git a/examples/LOCOSOxidation/LOCOSOxidation.cpp b/examples/LOCOSOxidation/LOCOSOxidation.cpp new file mode 100644 index 00000000..3abc752b --- /dev/null +++ b/examples/LOCOSOxidation/LOCOSOxidation.cpp @@ -0,0 +1,308 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace ls = viennals; + +using NumericType = double; +constexpr int D = 2; + +using LevelSet = ls::SmartPointer>; + +void writeSurface(LevelSet levelSet, const std::string &fileName) { + if (levelSet->getLevelSetWidth() < 2) + ls::Expand(levelSet, 2).apply(); + auto mesh = ls::Mesh::New(); + auto surfaceMesh = ls::ToSurfaceMesh(levelSet, mesh); + surfaceMesh.setSharpCorners(false); + surfaceMesh.apply(); + ls::VTKWriter(mesh, fileName).apply(); +} + +LevelSet makePlane(const double *bounds, + ls::Domain::BoundaryType *boundaryCons, + NumericType gridDelta, NumericType y) { + auto levelSet = + ls::Domain::New(bounds, boundaryCons, gridDelta); + const ls::VectorType origin{0., y}; + const ls::VectorType normal{0., 1.}; + ls::MakeGeometry( + levelSet, ls::Plane::New(origin, normal)) + .apply(); + return levelSet; +} + +LevelSet makeMask(const double *bounds, + ls::Domain::BoundaryType *boundaryCons, + NumericType gridDelta, NumericType xMin, NumericType xMax, + NumericType yMin, NumericType yMax) { + auto mask = ls::Domain::New(bounds, boundaryCons, gridDelta); + const ls::VectorType minCorner{xMin, yMin}; + const ls::VectorType maxCorner{xMax, yMax}; + ls::MakeGeometry makeGeometry( + mask, ls::Box::New(minCorner, maxCorner)); + makeGeometry.setIgnoreBoundaryConditions(std::array{false, true}); + makeGeometry.apply(); + return mask; +} + +void writeDiagnostics( + const ls::SmartPointer> &diffusion, + const ls::SmartPointer> + &deformation, + const viennahrle::Index &minIndex, const viennahrle::Index &maxIndex, + NumericType gridDelta, const std::string &fileName) { + std::ofstream file(fileName); + file << "x,y,concentration,velocity_x,velocity_y,pressure,strain_trace," + "von_mises_stress\n"; + for (auto j = minIndex[1]; j <= maxIndex[1]; ++j) { + for (auto i = minIndex[0]; i <= maxIndex[0]; ++i) { + ls::Vec3D coordinate{i * gridDelta, j * gridDelta, 0.}; + const auto velocity = deformation->getVelocity(coordinate); + file << coordinate[0] << ',' << coordinate[1] << ',' + << diffusion->getConcentration(coordinate) << ',' << velocity[0] + << ',' << velocity[1] << ',' << deformation->getPressure(coordinate) + << ',' << deformation->getStrainTrace(coordinate) << ',' + << deformation->getVonMisesStress(coordinate) << '\n'; + } + } +} + +int main(int argc, char *argv[]) { + omp_set_num_threads(4); + + // Pass "gpu" as first argument to use the GPU BiCGSTAB solver, e.g.: + // ./LOCOSOxidation gpu + // Requires a GPU build (-DVIENNALS_USE_GPU=ON). CPU is the default. + const bool useGpu = argc > 1 && std::string(argv[1]) == "gpu"; + + // Geometry β€” matches ViennaPS locosOxidation defaults. + constexpr NumericType gridDelta = 0.01; // um + constexpr NumericType xExtent = 1.; // um (REFLECTIVE at x=0) + constexpr NumericType yMin = -1.; // um + constexpr NumericType yMax = 2.; // um + constexpr NumericType padOxideThickness = 0.03; // um + constexpr NumericType maskThickness = 0.05; // um + constexpr NumericType maskEdge = 0.; // open window: x > 0 + constexpr NumericType advectionTime = 0.1; // hr + constexpr NumericType timeStep = 0.01; // hr per outer step + constexpr NumericType maskContactEpsilon = 1.e-6; // um + + double bounds[2 * D] = {-xExtent, xExtent, yMin, yMax}; + ls::Domain::BoundaryType boundaryCons[D]; + boundaryCons[0] = + ls::Domain::BoundaryType::REFLECTIVE_BOUNDARY; + boundaryCons[1] = ls::Domain::BoundaryType::INFINITE_BOUNDARY; + + // Si substrate at y = 0. + auto siInterface = makePlane(bounds, boundaryCons, gridDelta, 0.); + + // Pad SiO2: geometric offset of Si by padOxideThickness. + auto ambientInterface = ls::Domain::New(siInterface); + auto initialOxide = + ls::SmartPointer>::New( + padOxideThickness); + ls::GeometricAdvect(ambientInterface, initialOxide).apply(); + + // Capture pre-oxidation state for volume conservation check. + auto siInitialForConservation = ls::Domain::New(siInterface); + auto ambientInitialForConservation = + ls::Domain::New(ambientInterface); + + // Si3N4 mask box: covers x ∈ [βˆ’xExtent, maskEdge], sitting on the pad oxide. + // The tiny contact epsilon ensures Cartesian stencils unambiguously cross the + // mask boundary at the contact face. + auto maskInterface = + makeMask(bounds, boundaryCons, gridDelta, -xExtent, maskEdge, + padOxideThickness - maskContactEpsilon, + padOxideThickness + maskThickness); + + writeSurface(siInterface, "locos_si_initial.vtp"); + writeSurface(ambientInterface, "locos_ambient_initial.vtp"); + writeSurface(maskInterface, "locos_mask.vtp"); + + // ── Oxidation parameters ────────────────────────────────────────────────── + + auto oxParams = ls::OxidationPresets::wet1000CDealGrove100(); + oxParams.velocitySign = -1.; + oxParams.maskTransferCoefficient = 0.; // nitride is a perfect oxidant block + oxParams.maskConcentration = 0.; + oxParams.maxIterations = 10000; + oxParams.tolerance = 1.e-7; + + auto defParams = ls::OxidationPresets::oxideMechanics1000C(timeStep); + + // Coupling parameters β€” match ViennaPS locosOxidation config. + ls::OxidationCouplingParameters couplingParams; + couplingParams.maxIterations = 100; + couplingParams.tolerance = 2.e-2; + couplingParams.relaxation = 1.; + + auto maskParams = ls::OxidationPresets::siliconNitrideMask1000C(); + + // ── Solve bounds ────────────────────────────────────────────────────────── + + const auto toIndex = [&](NumericType x) { + return static_cast(std::llround(x / gridDelta)); + }; + + // Diffusion/deformation: covers the entire oxide-band region. + viennahrle::Index diffMinIndex{toIndex(-xExtent), toIndex(yMin)}; + viennahrle::Index diffMaxIndex{toIndex(xExtent), toIndex(yMax)}; + + // Mask bending: brackets the mask geometry with a one-cell margin. + viennahrle::Index maskMinIndex{toIndex(-xExtent), + toIndex(padOxideThickness) - 1}; + viennahrle::Index maskMaxIndex{ + toIndex(maskEdge), toIndex(padOxideThickness + maskThickness) + 1}; + + // ── LOCOS solver ────────────────────────────────────────────────────────── + + auto locos = ls::Oxidation::New(siInterface, ambientInterface, + maskInterface); + locos->setOxidationParameters(oxParams); + locos->setDeformationParameters(defParams); + locos->setCouplingParameters(couplingParams); + locos->setMaskParameters(maskParams); + locos->setSolveBounds(diffMinIndex, diffMaxIndex); + locos->setMaskBendingBounds(maskMinIndex, maskMaxIndex); + locos->setMaskCouplingIterations(30); + locos->setMaskCouplingTolerance(NumericType(1.e-2)); + if (useGpu) + locos->setGpuMode(ls::GpuMode::Gpu); + + // ── Time-stepping loop ──────────────────────────────────────────────────── + + NumericType elapsed = 0.; + const NumericType timeEps = advectionTime * NumericType(1e-8); + while (advectionTime - elapsed > timeEps) { + const NumericType dt = std::min(timeStep, advectionTime - elapsed); + elapsed += locos->applyCFLLimited(dt, NumericType(0.499)); + std::cout << "t = " << elapsed << " hr\n"; + } + + // ── Diagnostics ─────────────────────────────────────────────────────────── + + const auto diffusion = locos->getDiffusionField(); + const auto deformation = locos->getDeformationField(); + const auto maskBending = locos->getMaskBendingField(); + + // Sample points in open window and under mask. + const ls::Vec3D openReactionPoint{0.5, gridDelta, 0.}; + const ls::Vec3D maskedReactionPoint{-0.5, gridDelta, 0.}; + const NumericType openConcentration = + diffusion->getConcentration(openReactionPoint); + const NumericType maskedConcentration = + diffusion->getConcentration(maskedReactionPoint); + const NumericType openSiliconSpeed = std::abs( + diffusion->getScalarVelocity(openReactionPoint, 0, {0., 1., 0.}, 0)); + const NumericType maskedSiliconSpeed = std::abs( + diffusion->getScalarVelocity(maskedReactionPoint, 0, {0., 1., 0.}, 0)); + + std::cout << "Diffusion nodes: " << diffusion->getNumberOfSolutionNodes() + << ", iterations: " << diffusion->getIterations() + << ", residual: " << diffusion->getResidual() << '\n'; + std::cout << "Deformation nodes: " << deformation->getNumberOfSolutionNodes() + << ", iterations: " << deformation->getIterations() + << ", residual: " << deformation->getResidual() << '\n'; + std::cout << "Average oxide expansion speed: " + << deformation->avgExpansionSpeed() << " um/hr\n"; + std::cout << "Open-window concentration: " << openConcentration + << ", masked concentration: " << maskedConcentration << '\n'; + std::cout << "Open-window Si speed: " << openSiliconSpeed + << " um/hr, masked Si speed: " << maskedSiliconSpeed + << " um/hr, suppression ratio: " + << (openSiliconSpeed > 0. ? maskedSiliconSpeed / openSiliconSpeed + : 0.) + << '\n'; + + if (!std::isfinite(openSiliconSpeed) || !std::isfinite(maskedSiliconSpeed) || + openSiliconSpeed <= 0. || maskedSiliconSpeed / openSiliconSpeed > 0.05) { + std::cerr << "LOCOS mask sanity check failed: masked oxidation is not " + "sufficiently suppressed.\n"; + return 1; + } + + writeDiagnostics(diffusion, deformation, diffMinIndex, diffMaxIndex, + gridDelta, "locos_oxidation_diagnostics.csv"); + + const ls::Vec3D maskBottomSample{-0.5, padOxideThickness, 0.}; + const ls::Vec3D maskAnchorSample{ + -xExtent + gridDelta, padOxideThickness + maskThickness * 0.5, 0.}; + const ls::Vec3D maskMidSample{ + -0.5, padOxideThickness + maskThickness * 0.5, 0.}; + const ls::Vec3D maskEdgeSample{ + -gridDelta, padOxideThickness + maskThickness * 0.5, 0.}; + const ls::Vec3D maskContactNodeSample{-gridDelta, + padOxideThickness, 0.}; + const ls::Vec3D maskTopSample{ + -0.5, padOxideThickness + maskThickness, 0.}; + const auto maskBottomVelocity = + maskBending->getVectorVelocity(maskBottomSample, 0, {0., -1., 0.}, 0); + const auto maskTopVelocity = + maskBending->getVectorVelocity(maskTopSample, 0, {0., 1., 0.}, 0); + const auto maskContactNodeVelocity = maskBending->getVectorVelocity( + maskContactNodeSample, 0, {0., -1., 0.}, 0); + const auto maskAnchorVelocity = + maskBending->getVectorVelocity(maskAnchorSample, 0, {0., 1., 0.}, 0); + const auto maskMidVelocity = + maskBending->getVectorVelocity(maskMidSample, 0, {0., 1., 0.}, 0); + const auto maskEdgeVelocity = + maskBending->getVectorVelocity(maskEdgeSample, 0, {0., 1., 0.}, 0); + const NumericType maskBottomPressure = + deformation->getPressure(maskBottomSample); + std::cout << "Mask elasticity nodes: " + << maskBending->getNumberOfSolutionNodes() + << ", contact nodes: " << maskBending->getNumberOfContactNodes() + << ", iterations: " << maskBending->getIterations() + << ", residual: " << maskBending->getResidual() << '\n'; + std::cout << "Oxide/mask interface solve iterations: " + << locos->getMaskCouplingIterations() + << ", residual: " << locos->getMaskCouplingResidual() << '\n'; + std::cout << "Mask bottom velocity: (" << maskBottomVelocity[0] << ", " + << maskBottomVelocity[1] << ") um/hr, top velocity: (" + << maskTopVelocity[0] << ", " << maskTopVelocity[1] + << ") um/hr, oxide pressure: " << maskBottomPressure << " Pa\n"; + std::cout << "Mask contact-node velocity: (" << maskContactNodeVelocity[0] + << ", " << maskContactNodeVelocity[1] << ") um/hr\n"; + std::cout << "Mask lateral bending samples: anchor (" << maskAnchorVelocity[0] + << ", " << maskAnchorVelocity[1] << "), mid (" << maskMidVelocity[0] + << ", " << maskMidVelocity[1] << "), edge (" << maskEdgeVelocity[0] + << ", " << maskEdgeVelocity[1] << ") um/hr\n"; + + // Volume conservation in the open window. + const auto conservation = ls::computeLOCOSOpenWindowConservation( + siInitialForConservation, siInterface, ambientInitialForConservation, + ambientInterface, 0.1, 0.9, toIndex(yMin), toIndex(yMax), + oxParams.expansionCoefficient); + std::cout << "Open-window conservation samples: " << conservation.samples + << ", Si consumed area: " << conservation.siliconRecession + << " um^2, ambient lift area: " << conservation.ambientLift + << " um^2, expected ambient lift: " + << conservation.expectedAmbientLift + << " um^2, lift/Si ratio: " << conservation.ambientLiftRatio + << " (expected " << (oxParams.expansionCoefficient - 1.) + << "), relative error: " << conservation.relativeError << '\n'; + + writeSurface(siInterface, "locos_si_after.vtp"); + writeSurface(ambientInterface, "locos_ambient_after.vtp"); + writeSurface(maskInterface, "locos_mask_after.vtp"); + + std::cout << "Wrote locos_si_initial.vtp, locos_ambient_initial.vtp, " + "locos_mask.vtp, locos_si_after.vtp, locos_ambient_after.vtp, " + "locos_mask_after.vtp, and locos_oxidation_diagnostics.csv\n"; + + return 0; +} diff --git a/examples/LOCOSOxidation/LOCOSOxidation.py b/examples/LOCOSOxidation/LOCOSOxidation.py new file mode 100644 index 00000000..52b1abb4 --- /dev/null +++ b/examples/LOCOSOxidation/LOCOSOxidation.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +""" +LOCOS (Local Oxidation of Silicon) example β€” ViennaLS low-level API. + +Geometry (2-D cross-section): + Β· Si substrate at y = 0 + Β· Pad SiO2 of thickness padOxideThickness grown by geometric advect + Β· Si3N4 mask box covering x ∈ [βˆ’xExtent, maskEdge], sitting on pad oxide + +This script uses the ViennaLS Oxidation solver directly (lsOxidation), +bypassing ViennaPS. It mirrors LOCOSOxidation.cpp exactly. + +Usage: + python LOCOSOxidation.py + +All lengths are in micrometers, time in hours, pressure in Pa. +""" + +import math +import sys +import csv + +import viennals as vls + +vls.setDimension(2) + +# Pass "gpu" as first argument to use the GPU BiCGSTAB solver, e.g.: +# python LOCOSOxidation.py gpu +# Requires a GPU build (-DVIENNALS_USE_GPU=ON). CPU is the default. +USE_GPU = len(sys.argv) > 1 and sys.argv[1].lower() == "gpu" + +# ── Geometry constants ──────────────────────────────────────────────────────── +GRID_DELTA = 0.01 # um +X_EXTENT = 1.0 # um (REFLECTIVE at x = 0) +Y_MIN = -1.0 # um +Y_MAX = 2.0 # um +PAD_OXIDE_THICKNESS = 0.03 # um +MASK_THICKNESS = 0.05 # um +MASK_EDGE = 0.0 # um (open window: x > 0) +ADVECTION_TIME = 0.1 # hr +TIME_STEP = 0.01 # hr per outer step +MASK_CONTACT_EPS = 1.e-6 # um + + +def to_index(x: float) -> int: + return int(round(x / GRID_DELTA)) + + +def write_surface(level_set, file_name: str) -> None: + if level_set.getLevelSetWidth() < 2: + vls.Expand(level_set, 2).apply() + mesh = vls.Mesh() + surf = vls.ToSurfaceMesh(level_set, mesh) + surf.setSharpCorners(False) + surf.apply() + vls.VTKWriter(mesh, file_name).apply() + + +# ── Domain setup ────────────────────────────────────────────────────────────── +bounds = [-X_EXTENT, X_EXTENT, Y_MIN, Y_MAX] +bcs = [vls.BoundaryConditionEnum.REFLECTIVE_BOUNDARY, + vls.BoundaryConditionEnum.INFINITE_BOUNDARY] + +# Si substrate at y = 0. +si_interface = vls.Domain(bounds, bcs, GRID_DELTA) +vls.MakeGeometry( + si_interface, + vls.Plane([0., 0.], [0., 1.]), +).apply() + +# Pad SiO2: geometric offset of Si by padOxideThickness. +ambient_interface = vls.Domain(si_interface) +vls.GeometricAdvect( + ambient_interface, + vls.SphereDistribution(PAD_OXIDE_THICKNESS), +).apply() + +# Capture pre-oxidation state for volume conservation check. +si_initial = vls.Domain(si_interface) +ambient_initial = vls.Domain(ambient_interface) + +# Si3N4 mask box: covers x ∈ [βˆ’xExtent, maskEdge], sitting on the pad oxide. +mask_interface = vls.Domain(bounds, bcs, GRID_DELTA) +mask_geom = vls.MakeGeometry( + mask_interface, + vls.Box( + [-X_EXTENT, PAD_OXIDE_THICKNESS - MASK_CONTACT_EPS], + [MASK_EDGE, PAD_OXIDE_THICKNESS + MASK_THICKNESS], + ), +) +mask_geom.setIgnoreBoundaryConditions([False, True, False]) +mask_geom.apply() + +write_surface(si_interface, "locos_si_initial.vtp") +write_surface(ambient_interface, "locos_ambient_initial.vtp") +write_surface(mask_interface, "locos_mask.vtp") + +# ── Oxidation parameters ────────────────────────────────────────────────────── +ox_params = vls.OxidationPresets.wet1000CDealGrove100() +ox_params.velocitySign = -1. +ox_params.maskTransferCoefficient = 0. +ox_params.maskConcentration = 0. +ox_params.maxIterations = 10000 +ox_params.tolerance = 1.e-7 + +def_params = vls.OxidationPresets.oxideMechanics1000C(TIME_STEP) + +coupling_params = vls.OxidationCouplingParameters() +coupling_params.maxIterations = 100 +coupling_params.tolerance = 2.e-2 +coupling_params.relaxation = 1. + +mask_params = vls.OxidationPresets.siliconNitrideMask1000C() + +# ── Solve bounds ────────────────────────────────────────────────────────────── +diff_min = [to_index(-X_EXTENT), to_index(Y_MIN)] +diff_max = [to_index(X_EXTENT), to_index(Y_MAX)] + +mask_min = [to_index(-X_EXTENT), to_index(PAD_OXIDE_THICKNESS) - 1] +mask_max = [to_index(MASK_EDGE), to_index(PAD_OXIDE_THICKNESS + MASK_THICKNESS) + 1] + +# ── LOCOS solver ────────────────────────────────────────────────────────────── +locos = vls.Oxidation(si_interface, ambient_interface, mask_interface) +locos.setOxidationParameters(ox_params) +locos.setDeformationParameters(def_params) +locos.setCouplingParameters(coupling_params) +locos.setMaskParameters(mask_params) +locos.setSolveBounds(diff_min, diff_max) +locos.setMaskBendingBounds(mask_min, mask_max) +locos.setMaskCouplingIterations(30) +locos.setMaskCouplingTolerance(1.e-2) +if USE_GPU: + locos.setGpuMode(vls.GpuMode.Gpu) + +# ── Time-stepping loop ──────────────────────────────────────────────────────── +elapsed = 0.0 +time_eps = ADVECTION_TIME * 1.e-8 +while ADVECTION_TIME - elapsed > time_eps: + dt = min(TIME_STEP, ADVECTION_TIME - elapsed) + elapsed += locos.applyCFLLimited(dt, 0.499) + print(f"t = {elapsed:.6f} hr") + +# ── Diagnostics ─────────────────────────────────────────────────────────────── +diffusion = locos.getDiffusionField() +deformation = locos.getDeformationField() +mask_bending = locos.getMaskBendingField() + +open_pt = [0.5, GRID_DELTA, 0.] +masked_pt = [-0.5, GRID_DELTA, 0.] +normal_up = [0., 1., 0.] + +open_conc = diffusion.getConcentration(open_pt) +masked_conc = diffusion.getConcentration(masked_pt) +open_si_speed = abs(diffusion.getScalarVelocity(open_pt, 0, normal_up, 0)) +masked_si_speed = abs(diffusion.getScalarVelocity(masked_pt, 0, normal_up, 0)) +suppression_ratio = (masked_si_speed / open_si_speed) if open_si_speed > 0. else 0. + +print(f"Diffusion nodes: {diffusion.getNumberOfSolutionNodes()}, " + f"iters: {diffusion.getIterations()}, " + f"residual: {diffusion.getResidual():.3e}") +print(f"Deformation nodes: {deformation.getNumberOfSolutionNodes()}, " + f"iters: {deformation.getIterations()}, " + f"residual: {deformation.getResidual():.3e}") +print(f"Average oxide expansion speed: {deformation.avgExpansionSpeed():.6f} um/hr") +print(f"Open-window concentration: {open_conc:.6f}, " + f"masked concentration: {masked_conc:.6f}") +print(f"Open-window Si speed: {open_si_speed:.6f} um/hr, " + f"masked Si speed: {masked_si_speed:.6f} um/hr, " + f"suppression ratio: {suppression_ratio:.4f}") + +if not math.isfinite(open_si_speed) or not math.isfinite(masked_si_speed) \ + or open_si_speed <= 0. or suppression_ratio > 0.05: + print("LOCOS mask sanity check FAILED: masked oxidation is not sufficiently suppressed.", + file=sys.stderr) + sys.exit(1) + +# Write diagnostics CSV. +with open("locos_oxidation_diagnostics.csv", "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["x", "y", "concentration", "velocity_x", "velocity_y", + "pressure", "strain_trace", "von_mises_stress"]) + for j in range(diff_min[1], diff_max[1] + 1): + for i in range(diff_min[0], diff_max[0] + 1): + coord = [i * GRID_DELTA, j * GRID_DELTA, 0.] + vel = deformation.getVelocity(coord) + writer.writerow([ + coord[0], coord[1], + diffusion.getConcentration(coord), + vel[0], vel[1], + deformation.getPressure(coord), + deformation.getStrainTrace(coord), + deformation.getVonMisesStress(coord), + ]) + +# Mask bending samples. +normal_dn = [0., -1., 0.] +mask_pts = { + "bottom": ([-0.5, PAD_OXIDE_THICKNESS, 0.], normal_dn), + "top": ([-0.5, PAD_OXIDE_THICKNESS + MASK_THICKNESS, 0.], normal_up), + "contact_node": ([-GRID_DELTA, PAD_OXIDE_THICKNESS, 0.], normal_dn), + "anchor": ([-X_EXTENT + GRID_DELTA, + PAD_OXIDE_THICKNESS + MASK_THICKNESS * 0.5, 0.], normal_up), + "mid": ([-0.5, PAD_OXIDE_THICKNESS + MASK_THICKNESS * 0.5, 0.], normal_up), + "edge": ([-GRID_DELTA, PAD_OXIDE_THICKNESS + MASK_THICKNESS * 0.5, 0.], normal_up), +} + +vels = {name: mask_bending.getVectorVelocity(pt, 0, n, 0) + for name, (pt, n) in mask_pts.items()} +mask_bottom_pressure = deformation.getPressure(mask_pts["bottom"][0]) + +print(f"Mask elasticity nodes: {mask_bending.getNumberOfSolutionNodes()}, " + f"contact nodes: {mask_bending.getNumberOfContactNodes()}, " + f"iters: {mask_bending.getIterations()}, " + f"residual: {mask_bending.getResidual():.3e}") +print(f"Oxide/mask interface iters: {locos.getMaskCouplingIterations()}, " + f"residual: {locos.getMaskCouplingResidual():.3e}") +v = vels["bottom"] +print(f"Mask bottom velocity: ({v[0]:.6f}, {v[1]:.6f}) um/hr, " + f"top velocity: ({vels['top'][0]:.6f}, {vels['top'][1]:.6f}) um/hr, " + f"oxide pressure: {mask_bottom_pressure:.3e} Pa") +vc = vels["contact_node"] +print(f"Mask contact-node velocity: ({vc[0]:.6f}, {vc[1]:.6f}) um/hr") +va, vm, ve = vels["anchor"], vels["mid"], vels["edge"] +print(f"Mask lateral bending: anchor ({va[0]:.6f}, {va[1]:.6f}), " + f"mid ({vm[0]:.6f}, {vm[1]:.6f}), " + f"edge ({ve[0]:.6f}, {ve[1]:.6f}) um/hr") + +# ── Volume conservation ─────────────────────────────────────────────────────── +conservation = vls.computeLOCOSOpenWindowConservation( + si_initial, si_interface, + ambient_initial, ambient_interface, + 0.1, 0.9, + to_index(Y_MIN), to_index(Y_MAX), + ox_params.expansionCoefficient, +) +print(f"Open-window conservation samples: {conservation.samples}, " + f"Si consumed area: {conservation.siliconRecession:.6f} um^2, " + f"ambient lift area: {conservation.ambientLift:.6f} um^2, " + f"expected ambient lift: {conservation.expectedAmbientLift:.6f} um^2, " + f"lift/Si ratio: {conservation.ambientLiftRatio:.4f} " + f"(expected {ox_params.expansionCoefficient - 1.:.4f}), " + f"relative error: {conservation.relativeError:.4f}") + +# ── Final surface meshes ────────────────────────────────────────────────────── +write_surface(si_interface, "locos_si_after.vtp") +write_surface(ambient_interface, "locos_ambient_after.vtp") +write_surface(mask_interface, "locos_mask_after.vtp") + +print("Wrote locos_si_initial.vtp, locos_ambient_initial.vtp, locos_mask.vtp, " + "locos_si_after.vtp, locos_ambient_after.vtp, locos_mask_after.vtp, " + "and locos_oxidation_diagnostics.csv") diff --git a/gpu/lsOxidationBiCGSTABKernels.cu b/gpu/lsOxidationBiCGSTABKernels.cu new file mode 100644 index 00000000..99eb8778 --- /dev/null +++ b/gpu/lsOxidationBiCGSTABKernels.cu @@ -0,0 +1,199 @@ +// Compiled by nvcc. Provides C++ wrapper functions that delegate to the +// CUDA kernel implementations in lsOxidationBiCGSTAB.cuh. +// +// This translation unit is the ONLY place where lsOxidationBiCGSTAB.cuh +// is included; no .hpp/.cpp file compiled by g++ ever sees the __global__ +// kernel syntax. + +#include +#include + +#include +#include +#include +#include + +namespace viennals { +namespace gpu { + +namespace { +thread_local std::string lastGpuError; + +void setLastGpuError(const std::string& message) { + lastGpuError = message; + fprintf(stderr, "GpuBiCGSTABBuffers: %s\n", message.c_str()); +} + +void clearLastGpuError() { + lastGpuError.clear(); +} +} // namespace + +GpuBiCGSTABBuffers* allocGpuBuffers(uint32_t n, int nFaces, + bool useIlu0Preconditioner) { + clearLastGpuError(); + // Bail out if no device exists or if a CUDA context cannot actually be + // created (driver mismatch, permissions, etc.). cudaGetDeviceCount only + // enumerates devices; cudaFree(nullptr) forces real context initialization. + int deviceCount = 0; + const cudaError_t countErr = cudaGetDeviceCount(&deviceCount); + if (countErr != cudaSuccess) { + setLastGpuError(std::string("cudaGetDeviceCount failed: ") + + cudaGetErrorString(countErr)); + cudaGetLastError(); + return nullptr; + } + if (deviceCount == 0) { + setLastGpuError("cudaGetDeviceCount reported zero CUDA devices"); + return nullptr; + } + int deviceId = 0; + if (cudaGetDevice(&deviceId) != cudaSuccess || deviceId < 0 || + deviceId >= deviceCount) { + cudaGetLastError(); + deviceId = 0; + } + const cudaError_t setErr = cudaSetDevice(deviceId); + if (setErr != cudaSuccess) { + setLastGpuError(std::string("cudaSetDevice(") + + std::to_string(deviceId) + ") failed: " + + cudaGetErrorString(setErr)); + cudaGetLastError(); + return nullptr; + } + const cudaError_t ctxErr = cudaFree(nullptr); + if (ctxErr != cudaSuccess) { + setLastGpuError(std::string("CUDA context initialization failed on " + "device ") + + std::to_string(deviceId) + ": " + + cudaGetErrorString(ctxErr)); + cudaGetLastError(); // consume so later CUDA calls start clean + return nullptr; + } + + auto* b = new (std::nothrow) GpuBiCGSTABBuffers(); + if (!b) return nullptr; + b->deviceId = deviceId; + b->allocate(n, nFaces); + b->useIlu0Preconditioner = useIlu0Preconditioner; + if (!b->valid) { + delete b; + return nullptr; + } + return b; +} + +void freeGpuBuffers(GpuBiCGSTABBuffers* gpu) { + delete gpu; // calls ~GpuBiCGSTABBuffers() which calls gpu->free() +} + +bool gpuIsValid(const GpuBiCGSTABBuffers* gpu) { + return gpu != nullptr && gpu->valid; +} + +const char* gpuGetLastErrorMessage() { + return lastGpuError.c_str(); +} + +bool gpuUploadNeighborIds(GpuBiCGSTABBuffers* gpu, + const uint32_t* nb, + std::size_t count) { + const std::size_t expected = + static_cast(gpu->n) * static_cast(gpu->nFaces); + if (count != expected) { + setLastGpuError(std::string("neighbor ID upload length mismatch (got ") + + std::to_string(count) + ", expected " + + std::to_string(expected) + ")"); + return false; + } + if (!gpu->activateDevice("gpuUploadNeighborIds")) + return false; + return gpu->checkCuda( + cudaMemcpy(gpu->d_nb, nb, count * sizeof(uint32_t), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_nb)"); +} + +bool gpuUploadSolverArrays(GpuBiCGSTABBuffers* gpu, + const double* diag, + const double* b, + const double* coeff, + uint32_t diagLen, + std::size_t coeffLen) { + const std::size_t expectedCoeff = + static_cast(gpu->n) * static_cast(gpu->nFaces); + if (diagLen != gpu->n || coeffLen != expectedCoeff) { + setLastGpuError(std::string("solver array upload length mismatch " + "(diag got ") + + std::to_string(diagLen) + ", expected " + + std::to_string(gpu->n) + "; coeff got " + + std::to_string(coeffLen) + ", expected " + + std::to_string(expectedCoeff) + ")"); + return false; + } + if (!gpu->activateDevice("gpuUploadSolverArrays")) + return false; + if (!gpu->checkCuda(cudaMemcpy(gpu->d_diag, diag, diagLen * sizeof(double), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_diag)")) + return false; + if (!gpu->checkCuda(cudaMemcpy(gpu->d_b, b, diagLen * sizeof(double), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_b)")) + return false; + if (!gpu->checkCuda(cudaMemcpy(gpu->d_coeff, coeff, + coeffLen * sizeof(double), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_coeff)")) + return false; + if (!gpu->useIlu0Preconditioner) + return true; + // Refresh ILU(0) values and re-factorize whenever the coefficients change. + if (gpu->iluReady) + return gpu->fillAndFactorizeILU(diag, coeff, gpu->nFaces); + return false; +} + +bool gpuUploadRhs(GpuBiCGSTABBuffers* gpu, const double* b, uint32_t n) { + if (n != gpu->n) { + setLastGpuError(std::string("RHS upload length mismatch (got ") + + std::to_string(n) + ", expected " + + std::to_string(gpu->n) + ")"); + return false; + } + if (!gpu->activateDevice("gpuUploadRhs")) + return false; + return gpu->checkCuda( + cudaMemcpy(gpu->d_b, b, static_cast(n) * sizeof(double), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_b)"); +} + +bool gpuSetupCSR(GpuBiCGSTABBuffers* gpu, + const uint32_t* h_nb, + uint32_t n, + int nFaces) { + if (!gpu->activateDevice("gpuSetupCSR")) + return false; + return gpu->setupCSR(h_nb, n, nFaces); +} + +bool gpuSolveBiCGSTAB(GpuBiCGSTABBuffers* gpu, + double* x, + double diagEps, + unsigned maxIter, + double tolerance, + unsigned& outIterations, + double& outResidual) { + if (!gpu->activateDevice("gpuSolveBiCGSTAB")) + return false; + std::vector xVec(x, x + gpu->n); + const bool ok = solveBiCGSTAB(*gpu, xVec, diagEps, maxIter, tolerance, + outIterations, outResidual); + if (ok) + std::copy(xVec.begin(), xVec.end(), x); + return ok; +} + +} // namespace gpu +} // namespace viennals diff --git a/include/viennals/lsBooleanOperation.hpp b/include/viennals/lsBooleanOperation.hpp index 25d4ba1f..2595a95d 100644 --- a/include/viennals/lsBooleanOperation.hpp +++ b/include/viennals/lsBooleanOperation.hpp @@ -151,40 +151,48 @@ template class BooleanOperation { const auto &BData = levelSetB->getPointData(); auto &newData = newlsDomain->getPointData(); - // scalars + // scalars β€” preserve A's fields even when B doesn't have them. + // Previously, fields present only on A (e.g. OxVelocity on the oxide + // in a RELATIVE_COMPLEMENT with the mask) were silently dropped because + // the mask has no matching field. For A-sourced result points we always + // use A's value; for B-sourced points without a matching B field we use + // 0. for (unsigned i = 0; i < AData.getScalarDataSize(); ++i) { auto scalarDataLabel = AData.getScalarDataLabel(i); - auto BPointer = BData.getScalarData(scalarDataLabel, true); - if (BPointer != nullptr) { - auto APointer = AData.getScalarData(i); - // copy all data into the new scalarData - typename Domain::PointDataType::ScalarDataType scalars; - scalars.resize(newlsDomain->getNumberOfPoints()); - for (unsigned j = 0; j < newlsDomain->getNumberOfPoints(); ++j) { - scalars[j] = (newDataLS[0][j]) - ? APointer->at(newDataSourceIds[0][j]) - : BPointer->at(newDataSourceIds[0][j]); - } - newData.insertNextScalarData(scalars, scalarDataLabel); + auto APointer = AData.getScalarData(i); + auto BPointer = + BData.getScalarData(scalarDataLabel, true); // may be nullptr + typename Domain::PointDataType::ScalarDataType scalars; + scalars.resize(newlsDomain->getNumberOfPoints(), T(0)); + for (unsigned j = 0; j < newlsDomain->getNumberOfPoints(); ++j) { + if (newDataLS[0][j]) + scalars[j] = APointer->at(newDataSourceIds[0][j]); + else if (BPointer != nullptr) + scalars[j] = BPointer->at(newDataSourceIds[0][j]); + // else: B-sourced point with no matching B field β†’ stays 0 } + newData.insertNextScalarData(scalars, scalarDataLabel); } - // vectors + // vectors β€” same asymmetric treatment as scalars above. for (unsigned i = 0; i < AData.getVectorDataSize(); ++i) { auto vectorDataLabel = AData.getVectorDataLabel(i); - auto BPointer = BData.getVectorData(vectorDataLabel, true); - if (BPointer != nullptr) { - auto APointer = AData.getVectorData(i); - // copy all data into the new vectorData - typename Domain::PointDataType::VectorDataType vectors; - vectors.resize(newlsDomain->getNumberOfPoints()); - for (unsigned j = 0; j < newlsDomain->getNumberOfPoints(); ++j) { - vectors[j] = (newDataLS[0][j]) - ? APointer->at(newDataSourceIds[0][j]) - : BPointer->at(newDataSourceIds[0][j]); - } - newData.insertNextVectorData(vectors, vectorDataLabel); + auto APointer = AData.getVectorData(i); + auto BPointer = + BData.getVectorData(vectorDataLabel, true); // may be nullptr + using Vec = + typename Domain::PointDataType::VectorDataType::value_type; + typename Domain::PointDataType::VectorDataType vectors; + vectors.resize(newlsDomain->getNumberOfPoints(), Vec{}); + for (unsigned j = 0; j < newlsDomain->getNumberOfPoints(); ++j) { + if (newDataLS[0][j]) + vectors[j] = APointer->at(newDataSourceIds[0][j]); + else if (BPointer != nullptr) + vectors[j] = BPointer->at(newDataSourceIds[0][j]); + // else: B-sourced point with no matching B field β†’ stays + // zero-initialised } + newData.insertNextVectorData(vectors, vectorDataLabel); } } diff --git a/include/viennals/lsInterior.hpp b/include/viennals/lsInterior.hpp new file mode 100644 index 00000000..47df1cf4 --- /dev/null +++ b/include/viennals/lsInterior.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +namespace viennals { + +using namespace viennacore; + +/// Activates interior points in a level set. +/// The largest value in the levelset is thus width*0.5 +/// Returns the number of added points +template class Interior { + SmartPointer> levelSet = nullptr; + SmartPointer> guide = nullptr; + int width = 0; + bool updatePointData = true; + +public: + Interior() = default; + + Interior(SmartPointer> passedlsDomain) + : levelSet(passedlsDomain) {} + + void setLevelSet(SmartPointer> passedlsDomain) { + levelSet = passedlsDomain; + } + + /// Set a guide level set: the fill will not activate points that are inside + /// the guide (Ο†_guide < 0). Pass the Si surface to stop the oxide Interior + /// fill at the Si-SiO2 interface. + void setGuide(SmartPointer> g) { guide = g; } + + /// Set whether to update the point data stored in the LS + /// during this algorithm. Defaults to true. + void setUpdatePointData(bool update) { updatePointData = update; } + + /// Apply the interior definition until no more points can be added + void apply() { + if (levelSet == nullptr) { + Logger::getInstance() + .addError("No level set passed to Interior. Not activating.") + .print(); + return; + } + + if (levelSet->getNumberOfPoints() == 0) + return; + + const int startWidth = levelSet->getLevelSetWidth(); + + for (int currentCycle = 0; currentCycle < 100; ++currentCycle) { + int addedPoints = 0; + + const T limit = (startWidth + currentCycle + 1); + + auto &grid = levelSet->getGrid(); + auto newlsDomain = SmartPointer>::New(grid); + auto &newDomain = newlsDomain->getDomain(); + auto &domain = levelSet->getDomain(); + + newDomain.initialize(domain.getNewSegmentation(), domain.getAllocation()); + + const bool updateData = updatePointData; + // save how data should be transferred to new level set + // list of indices into the old pointData vector + std::vector> newDataSourceIds; + if (updateData) + newDataSourceIds.resize(newDomain.getNumberOfSegments()); + +#pragma omp parallel num_threads(newDomain.getNumberOfSegments()) \ + reduction(+ : addedPoints) + { + int p = 0; +#ifdef _OPENMP + p = omp_get_thread_num(); +#endif + + auto &domainSegment = newDomain.getDomainSegment(p); + + viennahrle::Index const startVector = + (p == 0) ? grid.getMinGridPoint() + : newDomain.getSegmentation()[p - 1]; + + viennahrle::Index const endVector = + (p != static_cast(newDomain.getNumberOfSegments() - 1)) + ? newDomain.getSegmentation()[p] + : grid.incrementIndices(grid.getMaxGridPoint()); + + // Per-thread guide iterator: advances in lock-step with main iterator. + using GuideIt = + viennahrle::ConstSparseIterator::DomainType>; + std::unique_ptr guideIt; + if (guide != nullptr) + guideIt = std::make_unique(guide->getDomain(), startVector); + + for (viennahrle::ConstSparseStarIterator< + typename Domain::DomainType, 1> + neighborIt(domain, startVector); + neighborIt.getIndices() < endVector; neighborIt.next()) { + + auto ¢erIt = neighborIt.getCenter(); + + // If a guide is set, block activation for points inside it (Ο† < 0). + bool insideGuide = false; + if (guideIt) { + guideIt->goToIndicesSequential(neighborIt.getIndices()); + insideGuide = (guideIt->getValue() < T(0)); + } + + if (centerIt.getValue() == Domain::NEG_VALUE) { + // Interior/negative side. Track the best *defined* neighbor + // separately: undefined sentinels (POS_VALUE / NEG_VALUE) must + // not be used as pointData sources β€” getPointId() on an + // undefined run returns an invalid index. + T distance = Domain::NEG_VALUE; + T definedDistance = Domain::NEG_VALUE; + int definedNeighbor = -1; + for (int i = 0; i < 2 * D; i++) { + auto &nb = neighborIt.getNeighbor(i); + T newValue = nb.getValue() - T(1); + if (distance < newValue) + distance = newValue; + if (nb.isDefined() && definedDistance < newValue) { + definedDistance = newValue; + definedNeighbor = i; + } + } + // Only activate when a proper defined neighbor drives the fill + // and the point is not inside the guide (e.g. Si substrate). + if (!insideGuide && definedDistance >= -limit && + definedNeighbor != -1) { + addedPoints++; + domainSegment.insertNextDefinedPoint(neighborIt.getIndices(), + definedDistance); + if (updateData) + newDataSourceIds[p].push_back( + neighborIt.getNeighbor(definedNeighbor).getPointId()); + } else { + // insertNextUndefinedRunType + domainSegment.insertNextUndefinedPoint(neighborIt.getIndices(), + Domain::NEG_VALUE); + } + } else if (centerIt.getValue() == Domain::POS_VALUE) { + // Ignore positive phi values (exterior region) + domainSegment.insertNextUndefinedPoint(neighborIt.getIndices(), + Domain::POS_VALUE); + } else { + domainSegment.insertNextDefinedPoint(neighborIt.getIndices(), + centerIt.getValue()); + if (updateData) + newDataSourceIds[p].push_back(centerIt.getPointId()); + } + } + } + + // now copy old data into new level set + if (updateData) { + newlsDomain->getPointData().translateFromMultiData( + levelSet->getPointData(), newDataSourceIds); + } + + newDomain.finalize(); + levelSet->deepCopy(newlsDomain); + if (addedPoints == 0) + break; + } + // segment() is intentionally skipped here. When the Interior fill + // produces a dense HRLE (many small runs from a deep fill), the + // balanced-partition boundaries computed by getNewSegmentation() land + // in the middle of existing runs. The SparseIterator inside segment() + // then starts at the run's true beginning (before the boundary), causing + // adjacent threads to overlap and producing a corrupt multi-segment domain + // that goToIndices later walks off. The single-segment domain left by + // the final finalize()+deepCopy() is valid for all downstream consumers: + // ConstSparseIterator in buildNodes(), writePersistentFields(), and + // lsAdvect (which re-segments its output internally). + } +}; + +// add all template specialisations for this class +PRECOMPILE_PRECISION_DIMENSION(Interior) + +} // namespace viennals diff --git a/include/viennals/lsOxidation.hpp b/include/viennals/lsOxidation.hpp new file mode 100644 index 00000000..436820d3 --- /dev/null +++ b/include/viennals/lsOxidation.hpp @@ -0,0 +1,819 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace viennals { + +using namespace viennacore; + +template struct LOCOSConservationDiagnostics { + T siliconRecession = 0.; + T ambientLift = 0.; + T expectedAmbientLift = 0.; + T ambientLiftRatio = 0.; + T relativeError = 0.; + unsigned samples = 0; +}; + +template +std::optional findLOCOSInterfaceY(SmartPointer> levelSet, + viennahrle::IndexType i, + viennahrle::IndexType jMin, + viennahrle::IndexType jMax) { + using ConstIterator = + viennahrle::ConstSparseIterator::DomainType>; + ConstIterator it(levelSet->getDomain()); + + const T gridDelta = levelSet->getGrid().getGridDelta(); + viennahrle::Index<2> previousIndex{i, jMin}; + it.goToIndices(previousIndex); + T previous = it.getValue(); + + for (auto j = jMin + 1; j <= jMax; ++j) { + viennahrle::Index<2> currentIndex{i, j}; + it.goToIndices(currentIndex); + const T current = it.getValue(); + if ((previous <= 0. && current >= 0.) || + (previous >= 0. && current <= 0.)) { + const T denominator = std::abs(previous) + std::abs(current); + const T fraction = denominator > std::numeric_limits::epsilon() + ? std::abs(previous) / denominator + : 0.; + return (static_cast(j - 1) + fraction) * gridDelta; + } + previous = current; + } + + return std::nullopt; +} + +/// Measure the open-window volume balance after one 2D LOCOS step. +/// +/// The diagnostic samples vertical columns, finds the Si/SiO2 and +/// oxide/ambient zero crossings, and compares ambient lift against the +/// Deal-Grove volume expansion expectation: +/// ambient lift = silicon consumed * (expansionCoefficient - 1) +template +LOCOSConservationDiagnostics computeLOCOSOpenWindowConservation( + SmartPointer> siInitial, SmartPointer> siAfter, + SmartPointer> ambientInitial, + SmartPointer> ambientAfter, T xMin, T xMax, + viennahrle::IndexType jMin, viennahrle::IndexType jMax, + T expansionCoefficient) { + LOCOSConservationDiagnostics result; + const T gridDelta = siInitial->getGrid().getGridDelta(); + const auto iMin = + static_cast(std::ceil(xMin / gridDelta)); + const auto iMax = + static_cast(std::floor(xMax / gridDelta)); + + for (auto i = iMin; i <= iMax; ++i) { + const auto si0 = findLOCOSInterfaceY(siInitial, i, jMin, jMax); + const auto si1 = findLOCOSInterfaceY(siAfter, i, jMin, jMax); + const auto amb0 = findLOCOSInterfaceY(ambientInitial, i, jMin, jMax); + const auto amb1 = findLOCOSInterfaceY(ambientAfter, i, jMin, jMax); + if (!si0 || !si1 || !amb0 || !amb1) + continue; + + result.siliconRecession += std::max(*si0 - *si1, T(0.)) * gridDelta; + result.ambientLift += std::max(*amb1 - *amb0, T(0.)) * gridDelta; + ++result.samples; + } + + result.expectedAmbientLift = + result.siliconRecession * (expansionCoefficient - T(1.)); + if (result.siliconRecession > 0.) + result.ambientLiftRatio = result.ambientLift / result.siliconRecession; + if (result.expectedAmbientLift > 0.) + result.relativeError = + std::abs(result.ambientLift - result.expectedAmbientLift) / + result.expectedAmbientLift; + return result; +} + +/// Unified oxidation time-step orchestrator. +/// +/// Couples diffusion, viscoelastic deformation, and (optionally) mask bending +/// into a single CFL-limited step. When a mask interface is provided the solver +/// operates in LOCOS mode; without one it performs standard planar or step +/// oxidation. +/// +/// Standard mode (no mask): +/// 1. Diffusion+deformation coupled solve (OxidationModel) +/// 2. Ambient interface advection (deformation velocity) +/// 3. Si/SiO2 reaction interface advection (diffusion velocity) +/// 4. Interior fill for warm-start persistence +/// +/// LOCOS mode (mask provided): +/// 1. Diffusion+deformation coupled solve (OxidationModel) +/// 2. Nitride mask bending solve (OxidationMaskBending) + coupling loop +/// 3. Pre-advection boolean clip (ambientInterface \ maskInterface) +/// 4. Ambient interface advection (constrained: mask-coupled under nitride) +/// 5. Si/SiO2 reaction interface advection (diffusion velocity) +/// 6. Mask advection (mask bending velocity) +/// 7. Interior fills + post-advection boolean clip +/// +/// Usage (standard): +/// @code +/// auto ox = ls::Oxidation::New(si, ambient); +/// ox->setOxidationParameters(oxParams); +/// ox->setDeformationParameters(defParams); +/// ox->apply(0.1); // advection time in hours +/// @endcode +/// +/// Usage (LOCOS): +/// @code +/// auto ox = ls::Oxidation::New(si, ambient, mask); +/// ox->setOxidationParameters(oxParams); +/// ox->setDeformationParameters(defParams); +/// ox->setMaskParameters(maskParams); +/// ox->setSolveBounds(minIdx, maxIdx); +/// ox->setMaskBendingBounds(maskMinIdx, maskMaxIdx); +/// ox->applyCFLLimited(0.35, 0.499); +/// ox->getDiffusionField()->getConcentration(pt); // diagnostics +/// @endcode +template class Oxidation { + using IndexType = viennahrle::Index; + + SmartPointer> siInterface = nullptr; + SmartPointer> ambientInterface = nullptr; + SmartPointer> maskInterface = nullptr; + + OxidationParameters oxidationParams; + OxidationDeformationParameters deformationParams; + OxidationCouplingParameters couplingParams; + OxidationMaskParameters maskParams; + + SpatialSchemeEnum spatialScheme = SpatialSchemeEnum::ENGQUIST_OSHER_1ST_ORDER; + TemporalSchemeEnum temporalScheme = TemporalSchemeEnum::FORWARD_EULER; + static constexpr int maskInteriorSign = -1; + unsigned maskCouplingIterations = 8; + T maskCouplingTolerance = 2.e-2; + unsigned lastMaskCouplingIterations = 0; + T lastMaskCouplingResidual = std::numeric_limits::max(); + + IndexType diffusionMinIndex{}; + IndexType diffusionMaxIndex{}; + bool diffusionBoundsSet = false; + + IndexType maskBendingMinIndex{}; + IndexType maskBendingMaxIndex{}; + bool maskBendingBoundsSet = false; + + // Populated by apply(); available for diagnostics afterwards. + SmartPointer> diffusionField; + SmartPointer> deformationField; + SmartPointer> maskBendingField; + T lastMaxVelocity_ = T(0); + + GpuMode gpuMode_ = GpuMode::Cpu; + GpuPreconditioner gpuPreconditioner_ = GpuPreconditioner::Jacobi; + std::unordered_map concentrationCache_; + +public: + const std::unordered_map &getConcentrationCache() const { + return concentrationCache_; + } + void setConcentrationCache(std::unordered_map cache) { + concentrationCache_ = std::move(cache); + } + + Oxidation() = default; + + Oxidation(SmartPointer> passedSiInterface, + SmartPointer> passedAmbientInterface, + SmartPointer> passedMaskInterface = nullptr) + : siInterface(passedSiInterface), + ambientInterface(passedAmbientInterface), + maskInterface(passedMaskInterface) {} + + template static auto New(Args &&...args) { + return SmartPointer::New(std::forward(args)...); + } + + void setGpuMode(GpuMode mode) { gpuMode_ = mode; } + void setGpuPreconditioner(GpuPreconditioner preconditioner) { + gpuPreconditioner_ = preconditioner; + } + + void setSiInterface(SmartPointer> si) { siInterface = si; } + void setAmbientInterface(SmartPointer> ambient) { + ambientInterface = ambient; + } + void setMaskInterface(SmartPointer> mask) { + maskInterface = mask; + } + + void setOxidationParameters(OxidationParameters params) { + oxidationParams = params; + } + void setDeformationParameters(OxidationDeformationParameters params) { + deformationParams = params; + } + void setCouplingParameters(OxidationCouplingParameters params) { + couplingParams = params; + } + void setMaskParameters(OxidationMaskParameters params) { + maskParams = params; + } + + /// Set the spatial integration scheme for all advections. + void setSpatialScheme(SpatialSchemeEnum scheme) { spatialScheme = scheme; } + + /// Set the temporal integration scheme for all advections. + void setTemporalScheme(TemporalSchemeEnum scheme) { temporalScheme = scheme; } + + void setMaskCouplingIterations(unsigned iterations) { + maskCouplingIterations = std::max(1u, iterations); + } + + void setMaskCouplingTolerance(T tolerance) { + maskCouplingTolerance = std::max(tolerance, T(0)); + } + + /// Set the Cartesian index bounding box for the diffusion and deformation + /// solves. If not set, bounds are auto-computed from the level-set narrow + /// band. + void setSolveBounds(const IndexType &minIndex, const IndexType &maxIndex) { + diffusionMinIndex = minIndex; + diffusionMaxIndex = maxIndex; + diffusionBoundsSet = true; + } + + /// Set the Cartesian index bounding box for the mask bending solve (LOCOS). + void setMaskBendingBounds(const IndexType &minIndex, + const IndexType &maxIndex) { + maskBendingMinIndex = minIndex; + maskBendingMaxIndex = maxIndex; + maskBendingBoundsSet = true; + } + + /// Return the diffusion field populated by the most recent apply() call. + SmartPointer> getDiffusionField() const { + return diffusionField; + } + + /// Return the deformation field populated by the most recent apply() call. + SmartPointer> getDeformationField() const { + return deformationField; + } + + /// Return the mask bending field (null when no mask is set). + SmartPointer> getMaskBendingField() const { + return maskBendingField; + } + + unsigned getMaskCouplingIterations() const { + return lastMaskCouplingIterations; + } + + T getMaskCouplingResidual() const { return lastMaskCouplingResidual; } + + /// Maximum interface velocity (Β΅m/hr) from the most recent CFL-limited step. + T getLastMaxVelocity() const { return lastMaxVelocity_; } + + /// Execute one oxidation time step of duration advectionTime. + void apply(T advectionTime) { applyImpl(advectionTime, std::nullopt); } + + /// Execute one CFL-limited oxidation step; returns the actual time advanced. + T applyCFLLimited(T requestedTime, T cflFactor) { + return applyImpl(requestedTime, std::clamp(cflFactor, T(1e-3), T(0.499))); + } + +private: + T applyImpl(T requestedTime, std::optional cflFactor) { + if (siInterface == nullptr || ambientInterface == nullptr) { + Logger::getInstance() + .addError("Oxidation: Si or ambient interface is null.") + .print(); + return T(0); + } + + if (requestedTime <= T(0)) + return T(0); + + Timer<> tStep; + tStep.start(); + + const bool hasMask = (maskInterface != nullptr); + const std::string prefix = hasMask ? "LOCOS" : "Oxidation"; + + VIENNACORE_LOG_INFO(prefix + ": starting time step, requested_dt=" + + std::to_string(requestedTime) + " hr"); + + const auto baseConcentrationCache = concentrationCache_; + std::string lastFieldFailureReason; + bool lastFailureWasMaskFixedPoint = false; + T adaptiveMaskRelaxationScale = T(1); + + auto solveFields = [&](T stressTimeStep, bool logCouplingResult) -> bool { + auto rejectSolve = [&](const std::string &reason) { + lastFieldFailureReason = reason; + return false; + }; + + auto validateCoupledModel = [&](const SmartPointer> + &model) { + if (!model->hasConverged()) { + const auto reason = model->getFailureReason(); + return rejectSolve( + reason.empty() + ? "pressure-concentration coupling failed (residual=" + + std::to_string(model->getResidual()) + ", tolerance=" + + std::to_string(couplingParams.tolerance) + ")" + : reason); + } + if (!diffusionField->lastSolveConverged() || + !diffusionField->hasFiniteConcentrationField()) { + return rejectSolve( + "diffusion solve failed (residual=" + + std::to_string(diffusionField->getNormalizedResidual()) + + ", tolerance=" + std::to_string(oxidationParams.tolerance) + ")"); + } + if (!deformationField->lastSolveConverged() || + !deformationField->hasFiniteSolution()) { + return rejectSolve( + "deformation solve failed (mechanics=" + + std::to_string(deformationField->getResidual()) + ", pressure=" + + std::to_string(deformationField->getLastPressureResidual()) + + ", stokes=" + + std::to_string(deformationField->getLastStokesResidual()) + ")"); + } + return true; + }; + + auto validateMaskSolve = [&]() { + if (maskBendingField == nullptr) + return true; + const T maskResidual = maskBendingField->getResidual(); + if (!std::isfinite(maskResidual) || + maskResidual > maskParams.tolerance) { + return rejectSolve("mask traction solve failed (residual=" + + std::to_string(maskResidual) + ", tolerance=" + + std::to_string(maskParams.tolerance) + ")"); + } + const T couplingResidual = + maskBendingField->getLastApplyVelocityChange(); + if (!std::isfinite(couplingResidual)) + return rejectSolve( + "mask velocity coupling produced non-finite values"); + return true; + }; + + lastFieldFailureReason.clear(); + lastFailureWasMaskFixedPoint = false; + auto stepDeformationParams = deformationParams; + stepDeformationParams.stressTimeStep = stressTimeStep; + + // --- Coupled diffusion + deformation solve --- + + diffusionField = OxidationDiffusion::New( + siInterface, ambientInterface, oxidationParams); + diffusionField->setConcentrationCache(baseConcentrationCache); + diffusionField->setGpuMode(gpuMode_); + diffusionField->setGpuPreconditioner(gpuPreconditioner_); + if (hasMask) + diffusionField->setMaskInterface(maskInterface, maskInteriorSign); + + deformationField = OxidationDeformation::New( + siInterface, ambientInterface, diffusionField, oxidationParams, + stepDeformationParams); + deformationField->setGpuMode(gpuMode_); + deformationField->setGpuPreconditioner(gpuPreconditioner_); + if (hasMask) + deformationField->setMaskInterface(maskInterface, maskInteriorSign); + + auto coupledModel = OxidationModel::New( + diffusionField, deformationField, couplingParams); + if (diffusionBoundsSet) + coupledModel->setSolveBounds(diffusionMinIndex, diffusionMaxIndex); + VIENNACORE_LOG_DEBUG( + prefix + ": solving coupled diffusion/deformation field for dt=" + + std::to_string(stressTimeStep) + " hr"); + Timer<> tCoupled; + tCoupled.start(); + coupledModel->apply(); + tCoupled.finish(); + if (Logger::hasTiming()) + Logger::getInstance().addTiming(" coupled(iter=1)", tCoupled).print(); + VIENNACORE_LOG_DEBUG(prefix + + ": coupled diffusion/deformation solve complete"); + if (!validateCoupledModel(coupledModel)) + return false; + + if (hasMask) { + // --- Mask bending solve --- + auto stepMaskParams = maskParams; + stepMaskParams.stressTimeStep = stressTimeStep; + stepMaskParams.relaxation = std::clamp( + maskParams.relaxation * adaptiveMaskRelaxationScale, T(0.01), T(1)); + maskBendingField = OxidationMaskBending::New( + deformationField, maskInterface, stepMaskParams, maskInteriorSign); + maskBendingField->setAmbientInterface(ambientInterface, + maskInteriorSign); + if (maskBendingBoundsSet) + maskBendingField->setSolveBounds(maskBendingMinIndex, + maskBendingMaxIndex); + VIENNACORE_LOG_DEBUG(prefix + ": solving mask bending field"); + Timer<> tMask; + tMask.start(); + try { + maskBendingField->apply(); + } catch (const std::exception &e) { + tMask.finish(); + return rejectSolve("mask bending solve error: " + + std::string(e.what())); + } + tMask.finish(); + if (Logger::hasTiming()) + Logger::getInstance() + .addTiming(" maskBending(iter=1)", tMask) + .print(); + if (!validateMaskSolve()) + return false; + T initialRes = maskBendingField->getLastApplyVelocityChange(); + VIENNACORE_LOG_DEBUG( + prefix + ": mask bending solve complete, residual=" + + (initialRes >= std::numeric_limits::max() * T(0.99) + ? std::string("initial") + : std::to_string(initialRes))); + + lastMaskCouplingIterations = 1; + lastMaskCouplingResidual = + maskBendingField->getLastApplyVelocityChange(); + deformationField->setMaskVelocityField(maskBendingField); + for (unsigned iteration = 1; iteration < maskCouplingIterations; + ++iteration) { + deformationField->setMaskVelocityField(maskBendingField); + VIENNACORE_LOG_DEBUG(prefix + ": coupling iteration " + + std::to_string(iteration + 1) + + " solving coupled field"); + Timer<> tIterCoupled, tIterMask; + tIterCoupled.start(); + coupledModel->apply(); + tIterCoupled.finish(); + if (!validateCoupledModel(coupledModel)) + return false; + VIENNACORE_LOG_DEBUG(prefix + ": coupling iteration " + + std::to_string(iteration + 1) + + " solving mask field"); + tIterMask.start(); + try { + maskBendingField->apply(); + } catch (const std::exception &e) { + tIterMask.finish(); + return rejectSolve("mask bending solve error at iteration " + + std::to_string(iteration + 1) + ": " + e.what()); + } + tIterMask.finish(); + if (!validateMaskSolve()) + return false; + if (Logger::hasTiming()) + Logger::getInstance() + .addTiming(" coupled(iter=" + std::to_string(iteration + 1) + + ")", + tIterCoupled) + .addTiming( + " maskBending(iter=" + std::to_string(iteration + 1) + ")", + tIterMask) + .print(); + lastMaskCouplingIterations = iteration + 1; + lastMaskCouplingResidual = + maskBendingField->getLastApplyVelocityChange(); + VIENNACORE_LOG_DEBUG( + prefix + ": coupling iteration " + std::to_string(iteration + 1) + + " residual=" + std::to_string(lastMaskCouplingResidual)); + if (lastMaskCouplingResidual <= maskCouplingTolerance) + break; + } + const T maskAbsoluteDisplacement = + maskBendingField->getLastApplyAbsoluteVelocityChange() * + stressTimeStep; + // Max physical displacement per step from the mask velocity field. + // Independent of coupling oscillation amplitude β€” the oscillation check + // (maskAbsoluteDisplacement) measures how much the velocity CHANGES + // between iterations; this measures how far the surface actually moves. + // When the active-set oscillates at a fixed amplitude, reducing dt + // does not reduce maskAbsoluteDisplacement, but it does reduce this. + T maxMaskVelocity = T(0); + for (unsigned d = 0; d < D; ++d) + maxMaskVelocity = + std::max(maxMaskVelocity, maskBendingField->getDissipationAlpha( + static_cast(d), -1, {})); + const T maskMaxDisplacement = maxMaskVelocity * stressTimeStep; + const T maskDisplacementTolerance = + maskCouplingTolerance * siInterface->getGrid().getGridDelta(); + const bool maskCouplingConverged = + lastMaskCouplingResidual <= maskCouplingTolerance || + (std::isfinite(maskAbsoluteDisplacement) && + maskAbsoluteDisplacement <= maskDisplacementTolerance) || + (std::isfinite(maskMaxDisplacement) && + maskMaxDisplacement <= maskDisplacementTolerance); + if (maskCouplingConverged) { + VIENNACORE_LOG_INFO( + prefix + ": mask/oxide coupling converged in " + + std::to_string(lastMaskCouplingIterations) + + " iterations (residual=" + + std::to_string(lastMaskCouplingResidual) + + ", displacement=" + std::to_string(maskAbsoluteDisplacement) + + " um, maxDisplacement=" + std::to_string(maskMaxDisplacement) + + " um)"); + } else if (logCouplingResult) { + VIENNACORE_LOG_WARNING( + prefix + + ": mask/oxide coupling did not converge " + "after " + + std::to_string(lastMaskCouplingIterations) + + " iterations (residual=" + + std::to_string(lastMaskCouplingResidual) + + ", displacement=" + std::to_string(maskAbsoluteDisplacement) + + " um" + ", tolerance=" + std::to_string(maskCouplingTolerance) + + "). Consider increasing maskCouplingIterations."); + } + if (!maskCouplingConverged) { + lastFailureWasMaskFixedPoint = true; + return rejectSolve( + "mask/oxide coupling failed (residual=" + + std::to_string(lastMaskCouplingResidual) + + ", displacement=" + std::to_string(maskAbsoluteDisplacement) + + " um, maxDisplacement=" + std::to_string(maskMaxDisplacement) + + " um" + ", tolerance=" + std::to_string(maskCouplingTolerance) + + ", relaxation=" + std::to_string(stepMaskParams.relaxation) + + ")"); + } + return true; + } else { + maskBendingField = nullptr; + } + + return true; + }; + + auto makeAmbientVelocity = [&]() -> SmartPointer> { + if (hasMask) { + return OxidationConstrainedAmbient::New( + deformationField, maskBendingField, maskInterface, ambientInterface, + maskInteriorSign); + } + return deformationField; + }; + + T advectionTime = requestedTime; + SmartPointer> ambientVelocity; + + auto computeMaxVelocity = + [&](const SmartPointer> &passedAmbientVelocity) { + T maxVelocity = diffusionField->getDissipationAlpha(0, -1, {}); + for (unsigned d = 0; d < D; ++d) { + maxVelocity = + std::max(maxVelocity, + passedAmbientVelocity->getDissipationAlpha(d, -1, {})); + if (hasMask) + maxVelocity = + std::max(maxVelocity, + maskBendingField->getDissipationAlpha(d, -1, {})); + } + return maxVelocity; + }; + + auto cflLimitedTime = [&](T trialTime, T maxVelocity) { + if (maxVelocity <= std::numeric_limits::epsilon()) + return trialTime; + const T gridDelta = siInterface->getGrid().getGridDelta(); + return std::min(trialTime, (*cflFactor) * gridDelta / maxVelocity); + }; + + if (cflFactor) { + T trialTime = requestedTime; + const T minTrialTime = std::max( + requestedTime * T(1e-10), std::numeric_limits::epsilon() * T(100)); + bool accepted = false; + for (unsigned attempt = 0; attempt < 16; ++attempt) { + const bool predictorConverged = solveFields(trialTime, false); + if (!predictorConverged) { + const bool dampMask = lastFailureWasMaskFixedPoint && + adaptiveMaskRelaxationScale > T(0.051); + if (dampMask) + adaptiveMaskRelaxationScale = + std::max(T(0.05), adaptiveMaskRelaxationScale * T(0.5)); + const T nextTrial = dampMask ? trialTime : trialTime * T(0.5); + VIENNACORE_LOG_INFO( + prefix + + ": rejecting non-converged coupled predictor " + "(" + + (lastFieldFailureReason.empty() + ? "mask residual=" + std::to_string(lastMaskCouplingResidual) + : lastFieldFailureReason) + + (dampMask ? ", retrying with mask relaxation scale=" + + std::to_string(adaptiveMaskRelaxationScale) + : std::string()) + + "), retrying with requested_dt=" + std::to_string(nextTrial) + + " hr"); + trialTime = nextTrial; + if (trialTime < minTrialTime) + break; + continue; + } + + ambientVelocity = makeAmbientVelocity(); + T maxVelocity = computeMaxVelocity(ambientVelocity); + if (!std::isfinite(maxVelocity)) + VIENNACORE_LOG_ERROR(prefix + ": non-finite CFL velocity estimate."); + + advectionTime = cflLimitedTime(trialTime, maxVelocity); + VIENNACORE_LOG_INFO( + prefix + + ": CFL decision requested_dt=" + std::to_string(trialTime) + + " hr, actual_dt=" + std::to_string(advectionTime) + + " hr, max_velocity=" + std::to_string(maxVelocity) + " um/hr"); + + if (advectionTime < trialTime * (T(1) - T(1e-8))) { + const bool finalConverged = solveFields(advectionTime, false); + if (!finalConverged) { + const bool dampMask = lastFailureWasMaskFixedPoint && + adaptiveMaskRelaxationScale > T(0.051); + if (dampMask) + adaptiveMaskRelaxationScale = + std::max(T(0.05), adaptiveMaskRelaxationScale * T(0.5)); + const T nextTrial = + dampMask ? advectionTime : advectionTime * T(0.5); + VIENNACORE_LOG_INFO( + prefix + + ": rejecting non-converged CFL re-solve " + "(" + + (lastFieldFailureReason.empty() + ? "mask residual=" + + std::to_string(lastMaskCouplingResidual) + : lastFieldFailureReason) + + (dampMask ? ", retrying with mask relaxation scale=" + + std::to_string(adaptiveMaskRelaxationScale) + : std::string()) + + "), retrying with requested_dt=" + std::to_string(nextTrial) + + " hr"); + trialTime = nextTrial; + if (trialTime < minTrialTime) + break; + continue; + } + ambientVelocity = makeAmbientVelocity(); + maxVelocity = computeMaxVelocity(ambientVelocity); + if (!std::isfinite(maxVelocity)) + VIENNACORE_LOG_ERROR(prefix + + ": non-finite accepted CFL velocity."); + + const T verifiedTime = cflLimitedTime(advectionTime, maxVelocity); + if (verifiedTime < advectionTime * (T(1) - T(1e-8))) { + VIENNACORE_LOG_INFO(prefix + + ": rejecting CFL re-solve because accepted " + "velocity requires requested_dt=" + + std::to_string(verifiedTime) + " hr"); + trialTime = verifiedTime; + if (trialTime < minTrialTime) + break; + continue; + } + } + + lastMaxVelocity_ = computeMaxVelocity(ambientVelocity); + accepted = true; + break; + } + if (!accepted) { + // Last resort: if the oxide solve (diffusion + deformation) is still + // finite β€” only the mask coupling diverged β€” freeze the mask for one + // minimal step so the geometry can evolve past the singular contact + // configuration. The mask doesn't move; the oxide advances at + // minTrialTime with no mask feedback this step. + const bool oxideFinite = + diffusionField && deformationField && + diffusionField->hasFiniteConcentrationField() && + deformationField->hasFiniteSolution(); + if (hasMask && oxideFinite) { + VIENNACORE_LOG_WARNING( + prefix + + ": all CFL attempts exhausted; freezing mask for " + "one step at dt=" + + std::to_string(minTrialTime) + + " hr (last failure: " + lastFieldFailureReason + + "). Consider increasing maskReferenceViscosity or " + "maskCouplingIterations."); + maskBendingField = nullptr; // skip mask advection this step + ambientVelocity = makeAmbientVelocity(); + advectionTime = minTrialTime; + lastMaxVelocity_ = computeMaxVelocity(ambientVelocity); + } else { + VIENNACORE_LOG_ERROR(prefix + + ": unable to find a converged CFL-limited step" + + (lastFieldFailureReason.empty() + ? std::string(".") + : std::string(" (last failure: ") + + lastFieldFailureReason + ").")); + } + } + } else { + const bool fieldsConverged = solveFields(requestedTime, true); + if (!fieldsConverged) + VIENNACORE_LOG_ERROR( + prefix + ": coupled solve failed" + + (lastFieldFailureReason.empty() + ? std::string(".") + : std::string(" (") + lastFieldFailureReason + ").")); + ambientVelocity = makeAmbientVelocity(); + } + + concentrationCache_ = diffusionField->getConcentrationCache(); + + diffusionField->markSolved(); + + // Pre-advection clip: keep oxide outside the mask body (LOCOS only). + if (hasMask) + BooleanOperation(ambientInterface, maskInterface, + BooleanOperationEnum::RELATIVE_COMPLEMENT) + .apply(); + + diffusionField->writePersistentFields(); + deformationField->writeFieldsToLevelSet(); + if (hasMask && maskBendingField) + maskBendingField->writeFieldsToLevelSet(); + + if (hasMask && maskBendingField) + maskBendingField->finalizeElasticAdvectionVelocity(); + + auto advect = [&](SmartPointer> levelSet, + SmartPointer> velocityField) { + Advect adv; + adv.insertNextLevelSet(levelSet); + adv.setVelocityField(velocityField); + adv.setSpatialScheme(spatialScheme); + adv.setTemporalScheme(temporalScheme); + adv.setAdvectionTime(advectionTime); + adv.apply(); + }; + + Timer<> tAdvect; + tAdvect.start(); + advect(ambientInterface, ambientVelocity); + advect(siInterface, diffusionField); + if (hasMask && maskBendingField) + advect(maskInterface, maskBendingField); + tAdvect.finish(); + VIENNACORE_LOG_TIMING(std::string(" advection(") + (hasMask ? "3" : "2") + + " surfaces)", + tAdvect); + + // Post-advection clip: remove oxide that grew into the mask (LOCOS only). + // Mask gets Interior fill first so the BooleanOp has accurate Ο†_mask values + // at points inside the mask region. + if (hasMask) { + Interior(maskInterface).apply(); + BooleanOperation(ambientInterface, maskInterface, + BooleanOperationEnum::RELATIVE_COMPLEMENT) + .apply(); + // Re-write mask velocity with the Interior-filled HRLE for the same + // reason as the oxide re-write below β€” lsAdvect left only the narrow + // band, so pointData is smaller than the post-Interior HRLE. + if (maskBendingField) + maskBendingField->writeFieldsToLevelSet(); + } + { + Interior fill(ambientInterface); + fill.setGuide(siInterface); // stop fill at Si surface + fill.apply(); + } + + // Re-write persistent fields (concentration, pressure) now that the HRLE + // has interior points. The first writePersistentFields() above only + // covered the narrow band; after lsAdvect the new HRLE is again a narrow + // band stripped of interior data. By re-writing here we ensure that the + // next outer step's buildNodes() can warm-start ALL oxide nodes β€” not just + // the surface band β€” when it reads back from pointData. + diffusionField->writePersistentFields(); + deformationField->writeFieldsToLevelSet(); + + VIENNACORE_LOG_INFO(prefix + ": time step complete, actual_dt=" + + std::to_string(advectionTime) + " hr"); + + tStep.finish(); + VIENNACORE_LOG_TIMING("── step total", tStep); + + return advectionTime; + } +}; + +} // namespace viennals diff --git a/include/viennals/lsOxidationBiCGSTAB.cuh b/include/viennals/lsOxidationBiCGSTAB.cuh new file mode 100644 index 00000000..3506575b --- /dev/null +++ b/include/viennals/lsOxidationBiCGSTAB.cuh @@ -0,0 +1,1363 @@ +// GPU-accelerated BiCGSTAB solver for the oxidation diffusion equation. +// +// Activated by defining VIENNALS_GPU_BICGSTAB (set when VIENNALS_USE_GPU is ON in CMake). +// Requires CUDA Toolkit; does NOT require OptiX. +// +// All kernels use the CUDA Runtime API (cuda_runtime.h) and are templated so +// they compile cleanly in multiple translation units. +// +// Memory layout (face-major): index = faceId * n + nodeId +// This gives coalesced reads when all warp threads process the same face +// of consecutive nodes. + +#pragma once + +#ifdef VIENNALS_GPU_BICGSTAB + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace viennals { +namespace gpu { + +// kNoNode (0xFFFFFFFFu) is declared in lsOxidationBiCGSTABInterface.hpp and +// used by callers. Kernels compare directly against the literal to avoid a +// duplicate-definition conflict when both headers are included in a .cu file. + +static constexpr int kThreadsPerBlock = 256; + +inline int blocksFor(int n) { + return (n + kThreadsPerBlock - 1) / kThreadsPerBlock; +} + +// ─────────────────────────── CUDA error check ──────────────────────────── + +#ifdef NDEBUG +#define LS_CUDA_CHECK(call) (call) +#else +#define LS_CUDA_CHECK(call) \ + do { \ + cudaError_t _e = (call); \ + if (_e != cudaSuccess) \ + fprintf(stderr, "CUDA error %s:%d – %s\n", __FILE__, __LINE__, \ + cudaGetErrorString(_e)); \ + } while (0) +#endif + +// ─────────────────────── CUSPARSE error check ──────────────────────────── + +#ifdef NDEBUG +#define LS_CUSPARSE_CHECK(call) (call) +#else +#define LS_CUSPARSE_CHECK(call) \ + do { \ + cusparseStatus_t _s = (call); \ + if (_s != CUSPARSE_STATUS_SUCCESS) \ + fprintf(stderr, "CUSPARSE error %s:%d – %d\n", __FILE__, \ + __LINE__, static_cast(_s)); \ + } while (0) +#endif + +// ─────────────────────────── kernels ───────────────────────────────────── + +// Sparse matrix-vector product: +// Av[i] = diag[i]*v[i] - sum_{fi: interior face}(coeff[fi*n+i] * v[nb[fi*n+i]]) +// +// nFaces is a compile-time constant so the inner loop can be unrolled. +// Boundary faces are represented by nb[fi*n+i] == kNoNode and coeff == 0, +// so the branch is always predicted correctly for regular grids. +template +__global__ void spmvKernel( + const double* __restrict__ v, + const double* __restrict__ diag, + const uint32_t* __restrict__ nb, + const double* __restrict__ coeff, + double* __restrict__ Av, + uint32_t n) +{ + const uint32_t i = blockIdx.x * blockDim.x + threadIdx.x; + if (i >= n) return; + + double res = diag[i] * v[i]; +#pragma unroll + for (int fi = 0; fi < nFaces; ++fi) { + const uint32_t j = nb[static_cast(fi) * n + i]; + if (j != 0xFFFFFFFFu) + res -= coeff[static_cast(fi) * n + i] * v[j]; + } + Av[i] = res; +} + +// r[i] = b[i] - Av[i] (initial residual setup) +__global__ void residualKernel( + const double* __restrict__ b, + const double* __restrict__ Av, + double* __restrict__ r, + uint32_t n) +{ + const uint32_t i = blockIdx.x * blockDim.x + threadIdx.x; + if (i >= n) return; + r[i] = b[i] - Av[i]; +} + +// p[i] = r[i] + beta*(p[i] - omega*v[i]) +__global__ void updatePKernel( + const double* __restrict__ r, + double beta, double beta_omega, + const double* __restrict__ v, + double* __restrict__ p, + uint32_t n) +{ + const uint32_t i = blockIdx.x * blockDim.x + threadIdx.x; + if (i >= n) return; + p[i] = r[i] + beta * p[i] - beta_omega * v[i]; +} + +// out[i] = in[i] - alpha*v[i] (used for s = r - alpha*v) +__global__ void subtractScaledKernel( + const double* __restrict__ in, + double alpha, + const double* __restrict__ v, + double* __restrict__ out, + uint32_t n) +{ + const uint32_t i = blockIdx.x * blockDim.x + threadIdx.x; + if (i >= n) return; + out[i] = in[i] - alpha * v[i]; +} + +// Jacobi preconditioner: out[i] = in[i] / diag[i] (guarded against zero diagonal) +__global__ void jacobiKernel( + const double* __restrict__ in, + const double* __restrict__ diag, + double* __restrict__ out, + double eps, + uint32_t n) +{ + const uint32_t i = blockIdx.x * blockDim.x + threadIdx.x; + if (i >= n) return; + out[i] = (diag[i] > eps) ? in[i] / diag[i] : in[i]; +} + +// x[i] += alpha*y[i] + omega*z[i] +__global__ void updateXKernel( + double* __restrict__ x, + double alpha, const double* __restrict__ y, + double omega, const double* __restrict__ z, + uint32_t n) +{ + const uint32_t i = blockIdx.x * blockDim.x + threadIdx.x; + if (i >= n) return; + x[i] += alpha * y[i] + omega * z[i]; +} + +// r[i] = s[i] - omega*t[i] (residual update at end of BiCGSTAB iteration) +__global__ void updateRKernel( + const double* __restrict__ s, + double omega, + const double* __restrict__ t, + double* __restrict__ r, + uint32_t n) +{ + const uint32_t i = blockIdx.x * blockDim.x + threadIdx.x; + if (i >= n) return; + r[i] = s[i] - omega * t[i]; +} + +// Atomic add for double. Native double atomicAdd requires sm_60; the project +// may be compiled for older compatibility architectures, so provide the +// standard CAS fallback. +__device__ inline double atomicAddDouble(double* address, double val) { +#if __CUDA_ARCH__ >= 600 + return atomicAdd(address, val); +#else + unsigned long long int* address_as_ull = + reinterpret_cast(address); + unsigned long long int old = *address_as_ull; + unsigned long long int assumed; + + do { + assumed = old; + old = atomicCAS(address_as_ull, assumed, + __double_as_longlong( + val + __longlong_as_double(assumed))); + } while (assumed != old); + + return __longlong_as_double(old); +#endif +} + +// Dot product: atomicAdd accumulates partial sums into result (must be zeroed before launch). +// Uses stride-loop so a fixed grid size (≀ 1024 blocks) handles any n. +__global__ void dotKernel( + const double* __restrict__ a, + const double* __restrict__ b, + double* __restrict__ result, + uint32_t n) +{ + extern __shared__ double sdata[]; + + const uint32_t tid = threadIdx.x; + double sum = 0.0; + for (uint32_t i = blockIdx.x * blockDim.x + tid; + i < n; + i += gridDim.x * blockDim.x) { + sum += a[i] * b[i]; + } + + sdata[tid] = sum; + __syncthreads(); + + for (unsigned s = blockDim.x >> 1; s > 0; s >>= 1) { + if (tid < s) sdata[tid] += sdata[tid + s]; + __syncthreads(); + } + + if (tid == 0) atomicAddDouble(result, sdata[0]); +} + +// Max absolute value using IEEE-754 bit trick: +// for non-negative doubles, bit-pattern ordering equals double ordering. +// result (unsigned long long*) must be zeroed before launch. +__global__ void maxAbsKernel( + const double* __restrict__ v, + unsigned long long int* __restrict__ result, + uint32_t n) +{ + extern __shared__ unsigned char smem[]; + unsigned long long int* sdata = + reinterpret_cast(smem); + + const uint32_t tid = threadIdx.x; + unsigned long long int maxVal = 0ull; + for (uint32_t i = blockIdx.x * blockDim.x + tid; + i < n; + i += gridDim.x * blockDim.x) { + unsigned long long int bits = + static_cast(__double_as_longlong(fabs(v[i]))); + if (bits > maxVal) maxVal = bits; + } + + sdata[tid] = maxVal; + __syncthreads(); + + for (unsigned s = blockDim.x >> 1; s > 0; s >>= 1) { + if (tid < s && sdata[tid + s] > sdata[tid]) + sdata[tid] = sdata[tid + s]; + __syncthreads(); + } + + if (tid == 0) atomicMax(result, sdata[0]); +} + +// ─────────────────────────── GPU buffer manager ─────────────────────────── + +// Owns all device-side memory for one solver instance. +// Geometry arrays (d_nb, d_diag, d_b, d_coeff) are uploaded once per +// buildNodes() call. Work vectors are reused across BiCGSTAB iterations +// within the same coupling sub-step. +struct GpuBiCGSTABBuffers { + // Geometry (uploaded once when the node grid is rebuilt) + double* d_diag = nullptr; + double* d_b = nullptr; + uint32_t* d_nb = nullptr; + double* d_coeff = nullptr; + + // BiCGSTAB work vectors (one double per node) + double* d_x = nullptr; + double* d_r = nullptr; + double* d_rhat = nullptr; + double* d_p = nullptr; + double* d_v = nullptr; + double* d_y = nullptr; + double* d_z = nullptr; + double* d_s = nullptr; + double* d_t = nullptr; + double* d_Ax = nullptr; + + // Scalar accumulators for reductions (one element each) + double* d_dot = nullptr; + unsigned long long int* d_maxu = nullptr; + + // ── ILU(0) preconditioner state ─────────────────────────────────────── + // CUSPARSE handles/descriptors (created once per buffer lifetime). + cusparseHandle_t csHandle = nullptr; + // Matrix descriptors: A (general, for ILU factorization), L, U (for SpSV). + cusparseMatDescr_t descrA = nullptr; // legacy descriptor; still used by csrilu02 + cusparseSpMatDescr_t spMatL = nullptr; // generic CSR descriptor for L factor + cusparseSpMatDescr_t spMatU = nullptr; // generic CSR descriptor for U factor + // ILU(0) factorization info and triangular-solve descriptors. + // csrilu02Info_t is deprecated in CUDA 12 but is the only supported + // in-place ILU(0) factorization interface; use it with the warning suppressed. + csrilu02Info_t iluInfo = nullptr; + cusparseSpSVDescr_t svDescrL = nullptr; // SpSV solve descriptor for L + cusparseSpSVDescr_t svDescrU = nullptr; // SpSV solve descriptor for U + // CSR arrays for the ILU(0) factor (pattern set in setupCSR; values refreshed each solve). + int* d_csrRowPtr = nullptr; // n+1 entries + int* d_csrColInd = nullptr; // nnz entries + double* d_iluVal = nullptr; // nnz entries β€” overwritten in-place by ILU(0) + // Intermediate vector for the two-step triangular solve: L*svTmp = rhs, U*out = svTmp. + double* d_svTmp = nullptr; // n entries + // CUSPARSE internal work buffers (sizes determined at analysis time). + void* d_iluBuf = nullptr; + void* d_svBufL = nullptr; + void* d_svBufU = nullptr; + size_t svBufSizeLBytes = 0; + size_t svBufSizeUBytes = 0; + // Number of non-zeros in the ILU pattern. + int nnz = 0; + // True when setupCSR and analysis have completed successfully. + bool iluReady = false; + // Host-side CSR index arrays (kept for fillAndFactorizeILU value assembly). + std::vector h_csrRowPtr; + std::vector h_csrColInd; + // h_csrFaceIdx[k] = face index fi for off-diagonal entry k, or -1 for diagonal. + std::vector h_csrFaceIdx; + + uint32_t n = 0; + int nFaces = 0; + int deviceId = 0; + bool useIlu0Preconditioner = false; + bool valid = false; + + // Non-copyable + GpuBiCGSTABBuffers() = default; + GpuBiCGSTABBuffers(const GpuBiCGSTABBuffers&) = delete; + GpuBiCGSTABBuffers& operator=(const GpuBiCGSTABBuffers&) = delete; + + ~GpuBiCGSTABBuffers() { free(); } + + bool checkCuda(cudaError_t err, const char* what) const { + if (err == cudaSuccess) + return true; + fprintf(stderr, "GpuBiCGSTABBuffers: %s failed on CUDA device %d: %s\n", + what, deviceId, cudaGetErrorString(err)); + cudaGetLastError(); + return false; + } + + bool checkCusparse(cusparseStatus_t status, const char* what) const { + if (status == CUSPARSE_STATUS_SUCCESS) + return true; + fprintf(stderr, "GpuBiCGSTABBuffers: %s failed on CUDA device %d " + "(CUSPARSE status %d)\n", + what, deviceId, static_cast(status)); + return false; + } + + bool activateDevice(const char* where) const { + const cudaError_t setErr = cudaSetDevice(deviceId); + if (setErr != cudaSuccess) { + fprintf(stderr, + "GpuBiCGSTABBuffers: %s could not set CUDA device %d: %s\n", + where, deviceId, cudaGetErrorString(setErr)); + cudaGetLastError(); + return false; + } + const cudaError_t ctxErr = cudaFree(nullptr); + if (ctxErr != cudaSuccess) { + fprintf(stderr, + "GpuBiCGSTABBuffers: %s could not initialize CUDA context " + "on device %d: %s\n", + where, deviceId, cudaGetErrorString(ctxErr)); + cudaGetLastError(); + return false; + } + return true; + } + + void free() { + cudaSetDevice(deviceId); + // Destroy CUSPARSE objects before freeing device memory. + iluReady = false; + if (svDescrU) { cusparseSpSV_destroyDescr(svDescrU); svDescrU = nullptr; } + if (svDescrL) { cusparseSpSV_destroyDescr(svDescrL); svDescrL = nullptr; } + if (spMatU) { cusparseDestroySpMat(spMatU); spMatU = nullptr; } + if (spMatL) { cusparseDestroySpMat(spMatL); spMatL = nullptr; } + if (iluInfo) { cusparseDestroyCsrilu02Info(iluInfo); iluInfo = nullptr; } + if (descrA) { cusparseDestroyMatDescr(descrA); descrA = nullptr; } + if (csHandle) { cusparseDestroy(csHandle); csHandle = nullptr; } + + auto f = [](auto*& p) { if (p) { cudaFree(p); p = nullptr; } }; + f(d_iluBuf); f(d_svBufL); f(d_svBufU); + svBufSizeLBytes = 0; + svBufSizeUBytes = 0; + f(d_svTmp); f(d_iluVal); f(d_csrColInd); f(d_csrRowPtr); + f(d_diag); f(d_b); f(d_nb); f(d_coeff); + f(d_x); f(d_r); f(d_rhat); f(d_p); f(d_v); + f(d_y); f(d_z); f(d_s); f(d_t); f(d_Ax); + f(d_dot); f(d_maxu); + nnz = 0; + h_csrRowPtr.clear(); h_csrColInd.clear(); h_csrFaceIdx.clear(); + n = 0; nFaces = 0; useIlu0Preconditioner = false; valid = false; + } + + void allocate(uint32_t nodeCount, int faceCount) { + free(); + if (!activateDevice("allocate")) + return; + n = nodeCount; + nFaces = faceCount; + const size_t N = n; + const size_t NF = N * static_cast(nFaces); + // Every cudaMalloc is checked; any failure marks the buffers invalid. + // This prevents the GPU path from being entered with null device pointers + // when the CUDA context cannot be initialized (e.g. driver mismatch). +#define TRY_MALLOC(ptr, sz) \ + if (!checkCuda(cudaMalloc(&(ptr), (sz)), "cudaMalloc(" #ptr ")")) return; + TRY_MALLOC(d_diag, N * sizeof(double)) + TRY_MALLOC(d_b, N * sizeof(double)) + TRY_MALLOC(d_nb, NF * sizeof(uint32_t)) + TRY_MALLOC(d_coeff, NF * sizeof(double)) + TRY_MALLOC(d_x, N * sizeof(double)) + TRY_MALLOC(d_r, N * sizeof(double)) + TRY_MALLOC(d_rhat, N * sizeof(double)) + TRY_MALLOC(d_p, N * sizeof(double)) + TRY_MALLOC(d_v, N * sizeof(double)) + TRY_MALLOC(d_y, N * sizeof(double)) + TRY_MALLOC(d_z, N * sizeof(double)) + TRY_MALLOC(d_s, N * sizeof(double)) + TRY_MALLOC(d_t, N * sizeof(double)) + TRY_MALLOC(d_Ax, N * sizeof(double)) + TRY_MALLOC(d_dot, sizeof(double)) + TRY_MALLOC(d_maxu, sizeof(unsigned long long int)) +#undef TRY_MALLOC + valid = true; + } + + // ── primitive reductions ─────────────────────────────────────────── + + // Dot product: result = sum(a[i] * b[i]). + double dotProduct(const double* a, const double* b) const { + if (!checkCuda(cudaSetDevice(deviceId), "cudaSetDevice(dotProduct)")) + return NAN; + if (!checkCuda(cudaMemset(d_dot, 0, sizeof(double)), + "cudaMemset(d_dot)")) + return NAN; + const int blk = std::min(blocksFor(static_cast(n)), 1024); + dotKernel<<>>(a, b, d_dot, n); + if (!checkCuda(cudaPeekAtLastError(), "dotKernel")) + return NAN; + double result = 0.0; + if (!checkCuda(cudaMemcpy(&result, d_dot, sizeof(double), + cudaMemcpyDeviceToHost), + "cudaMemcpy(d_dot)")) + return NAN; + return result; + } + + // Max absolute value: result = max(|v[i]|) + double maxAbs(const double* v) const { + if (!checkCuda(cudaSetDevice(deviceId), "cudaSetDevice(maxAbs)")) + return NAN; + if (!checkCuda(cudaMemset(d_maxu, 0, sizeof(unsigned long long int)), + "cudaMemset(d_maxu)")) + return NAN; + const int blk = std::min(blocksFor(static_cast(n)), 1024); + maxAbsKernel<<>>( + v, d_maxu, n); + if (!checkCuda(cudaPeekAtLastError(), "maxAbsKernel")) + return NAN; + unsigned long long int bits = 0ull; + if (!checkCuda(cudaMemcpy(&bits, d_maxu, sizeof(unsigned long long int), + cudaMemcpyDeviceToHost), + "cudaMemcpy(d_maxu)")) + return NAN; + // Reverse the IEEE-754 bit-cast trick used in the kernel + double result; + __builtin_memcpy(&result, &bits, sizeof(double)); + return result; + } + + // ── SpMV dispatch ────────────────────────────────────────────────── + + bool spmv(const double* v_in, double* Av_out) const { + if (!checkCuda(cudaSetDevice(deviceId), "cudaSetDevice(spmv)")) + return false; + const int blk = std::min(blocksFor(static_cast(n)), 65535); + if (nFaces == 4) + spmvKernel<4><<>>( + v_in, d_diag, d_nb, d_coeff, Av_out, n); + else + spmvKernel<6><<>>( + v_in, d_diag, d_nb, d_coeff, Av_out, n); + return checkCuda(cudaPeekAtLastError(), "spmvKernel"); + } + + // ── ILU(0) setup ─────────────────────────────────────────────────── + + // Build the CSR sparsity pattern from the face-major neighbor array h_nb, + // upload index arrays to the device, create CUSPARSE handles/descriptors, + // and run symbolic analysis for both the ILU(0) factorization and the + // two triangular solves. Called once per buildNodes() cycle. + // h_nb is the nb32 array in face-major layout: h_nb[fi * nodeCount + i]. + bool setupCSR(const uint32_t* h_nb, uint32_t nodeCount, int faceCount) { + if (!activateDevice("setupCSR")) + return false; + + // Destroy any previous ILU state but keep the main CUDA allocations. + iluReady = false; + if (svDescrU) { cusparseSpSV_destroyDescr(svDescrU); svDescrU = nullptr; } + if (svDescrL) { cusparseSpSV_destroyDescr(svDescrL); svDescrL = nullptr; } + if (spMatU) { cusparseDestroySpMat(spMatU); spMatU = nullptr; } + if (spMatL) { cusparseDestroySpMat(spMatL); spMatL = nullptr; } + if (iluInfo) { cusparseDestroyCsrilu02Info(iluInfo); iluInfo = nullptr; } + if (descrA) { cusparseDestroyMatDescr(descrA); descrA = nullptr; } + if (csHandle) { cusparseDestroy(csHandle); csHandle = nullptr; } + if (d_iluBuf) { cudaFree(d_iluBuf); d_iluBuf = nullptr; } + if (d_svBufL) { cudaFree(d_svBufL); d_svBufL = nullptr; svBufSizeLBytes = 0; } + if (d_svBufU) { cudaFree(d_svBufU); d_svBufU = nullptr; svBufSizeUBytes = 0; } + if (d_svTmp) { cudaFree(d_svTmp); d_svTmp = nullptr; } + if (d_iluVal) { cudaFree(d_iluVal); d_iluVal = nullptr; } + if (d_csrColInd) { cudaFree(d_csrColInd); d_csrColInd = nullptr; } + if (d_csrRowPtr) { cudaFree(d_csrRowPtr); d_csrRowPtr = nullptr; } + h_csrRowPtr.clear(); h_csrColInd.clear(); h_csrFaceIdx.clear(); + nnz = 0; + + // ── Build sorted CSR on CPU ──────────────────────────────────── + // For each row i, collect: the diagonal entry (col = i, faceIdx = -1) + // and all valid off-diagonal entries (col = h_nb[fi*n+i], faceIdx = fi). + // Then sort by column index. + const uint32_t N = nodeCount; + h_csrRowPtr.resize(static_cast(N) + 1, 0); + + // First pass: count entries per row. + for (uint32_t i = 0; i < N; ++i) { + int cnt = 1; // diagonal always present + for (int fi = 0; fi < faceCount; ++fi) { + const uint32_t j = h_nb[static_cast(fi) * N + i]; + if (j != 0xFFFFFFFFu) + ++cnt; + } + h_csrRowPtr[i + 1] = cnt; + } + // Prefix-sum into row pointers. + for (uint32_t i = 0; i < N; ++i) + h_csrRowPtr[i + 1] += h_csrRowPtr[i]; + + const int totalNnz = h_csrRowPtr[N]; + h_csrColInd.resize(static_cast(totalNnz)); + h_csrFaceIdx.resize(static_cast(totalNnz)); + + // Second pass: fill column indices and face indices. + // Use a temporary write-pointer per row. + std::vector writePtr(h_csrRowPtr.begin(), h_csrRowPtr.begin() + N); + for (uint32_t i = 0; i < N; ++i) { + const int base = writePtr[i]; + int k = base; + // Diagonal entry first (will sort to correct position). + h_csrColInd[k] = static_cast(i); + h_csrFaceIdx[k] = -1; + ++k; + // Off-diagonal entries. + for (int fi = 0; fi < faceCount; ++fi) { + const uint32_t j = h_nb[static_cast(fi) * N + i]; + if (j != 0xFFFFFFFFu) { + h_csrColInd[k] = static_cast(j); + h_csrFaceIdx[k] = fi; + ++k; + } + } + const int rowEnd = h_csrRowPtr[i + 1]; + const int rowLen = rowEnd - base; + // Sort the row's entries by column index (insertion sort β€” rows are small). + for (int a = 1; a < rowLen; ++a) { + const int colA = h_csrColInd[base + a]; + const int faceA = h_csrFaceIdx[base + a]; + int b = a - 1; + while (b >= 0 && h_csrColInd[base + b] > colA) { + h_csrColInd[base + b + 1] = h_csrColInd[base + b]; + h_csrFaceIdx[base + b + 1] = h_csrFaceIdx[base + b]; + --b; + } + h_csrColInd[base + b + 1] = colA; + h_csrFaceIdx[base + b + 1] = faceA; + } + } + nnz = totalNnz; + + // ── Upload CSR index arrays to device ────────────────────────── + if (!checkCuda(cudaMalloc(&d_csrRowPtr, + (static_cast(N) + 1) * sizeof(int)), + "cudaMalloc(d_csrRowPtr)")) + return false; + if (!checkCuda(cudaMalloc(&d_csrColInd, + static_cast(nnz) * sizeof(int)), + "cudaMalloc(d_csrColInd)")) + return false; + if (!checkCuda(cudaMalloc(&d_iluVal, + static_cast(nnz) * sizeof(double)), + "cudaMalloc(d_iluVal)")) + return false; + if (!checkCuda(cudaMalloc(&d_svTmp, + static_cast(N) * sizeof(double)), + "cudaMalloc(d_svTmp)")) + return false; + + if (!checkCuda(cudaMemcpy(d_csrRowPtr, h_csrRowPtr.data(), + (static_cast(N) + 1) * sizeof(int), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_csrRowPtr)")) + return false; + if (!checkCuda(cudaMemcpy(d_csrColInd, h_csrColInd.data(), + static_cast(nnz) * sizeof(int), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_csrColInd)")) + return false; + + // Fill placeholder values so that analysis can proceed: + // diagonal = 1.0, off-diagonal = -1.0 / faceCount. + { + const double offVal = -1.0 / static_cast(faceCount > 0 ? faceCount : 1); + std::vector hPlaceholder(static_cast(nnz)); + for (uint32_t i = 0; i < N; ++i) { + for (int k = h_csrRowPtr[i]; k < h_csrRowPtr[i + 1]; ++k) { + hPlaceholder[k] = (h_csrColInd[k] == static_cast(i)) + ? 1.0 : offVal; + } + } + if (!checkCuda(cudaMemcpy(d_iluVal, hPlaceholder.data(), + static_cast(nnz) * sizeof(double), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_iluVal placeholder)")) + return false; + } + + // ── Create CUSPARSE handle and descriptors ──────────────────── + if (!checkCusparse(cusparseCreate(&csHandle), "cusparseCreate")) + return false; + + if (!checkCusparse(cusparseCreateMatDescr(&descrA), + "cusparseCreateMatDescr")) return false; + if (!checkCusparse(cusparseSetMatType(descrA, + CUSPARSE_MATRIX_TYPE_GENERAL), + "cusparseSetMatType(descrA)")) return false; + if (!checkCusparse(cusparseSetMatIndexBase(descrA, + CUSPARSE_INDEX_BASE_ZERO), + "cusparseSetMatIndexBase(descrA)")) return false; + + // ── Create ILU(0) info (legacy API, still present in CUDA 12) ──── +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + if (!checkCusparse(cusparseCreateCsrilu02Info(&iluInfo), + "cusparseCreateCsrilu02Info")) return false; +#pragma GCC diagnostic pop + + // ── Create generic SpMat descriptors for L and U (CUDA 12 SpSV API) ─ + // Both L and U share d_iluVal (in-place ILU factorization). + cusparseFillMode_t fillL = CUSPARSE_FILL_MODE_LOWER; + cusparseDiagType_t diagL = CUSPARSE_DIAG_TYPE_UNIT; + cusparseFillMode_t fillU = CUSPARSE_FILL_MODE_UPPER; + cusparseDiagType_t diagU = CUSPARSE_DIAG_TYPE_NON_UNIT; + + if (!checkCusparse( + cusparseCreateCsr(&spMatL, static_cast(N), + static_cast(N), + static_cast(nnz), + d_csrRowPtr, d_csrColInd, d_iluVal, + CUSPARSE_INDEX_32I, CUSPARSE_INDEX_32I, + CUSPARSE_INDEX_BASE_ZERO, CUDA_R_64F), + "cusparseCreateCsr(L)")) return false; + if (!checkCusparse( + cusparseSpMatSetAttribute(spMatL, CUSPARSE_SPMAT_FILL_MODE, + static_cast(&fillL), + sizeof(fillL)), + "cusparseSpMatSetAttribute(L fill mode)")) return false; + if (!checkCusparse( + cusparseSpMatSetAttribute(spMatL, CUSPARSE_SPMAT_DIAG_TYPE, + static_cast(&diagL), + sizeof(diagL)), + "cusparseSpMatSetAttribute(L diag type)")) return false; + + if (!checkCusparse( + cusparseCreateCsr(&spMatU, static_cast(N), + static_cast(N), + static_cast(nnz), + d_csrRowPtr, d_csrColInd, d_iluVal, + CUSPARSE_INDEX_32I, CUSPARSE_INDEX_32I, + CUSPARSE_INDEX_BASE_ZERO, CUDA_R_64F), + "cusparseCreateCsr(U)")) return false; + if (!checkCusparse( + cusparseSpMatSetAttribute(spMatU, CUSPARSE_SPMAT_FILL_MODE, + static_cast(&fillU), + sizeof(fillU)), + "cusparseSpMatSetAttribute(U fill mode)")) return false; + if (!checkCusparse( + cusparseSpMatSetAttribute(spMatU, CUSPARSE_SPMAT_DIAG_TYPE, + static_cast(&diagU), + sizeof(diagU)), + "cusparseSpMatSetAttribute(U diag type)")) return false; + + if (!checkCusparse(cusparseSpSV_createDescr(&svDescrL), + "cusparseSpSV_createDescr(L)")) return false; + if (!checkCusparse(cusparseSpSV_createDescr(&svDescrU), + "cusparseSpSV_createDescr(U)")) return false; + + const int iN = static_cast(N); + + // ── ILU(0) analysis ─────────────────────────────────────────── + int iluBufSize = 0; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + cusparseStatus_t st = cusparseDcsrilu02_bufferSize( + csHandle, iN, nnz, descrA, + d_iluVal, d_csrRowPtr, d_csrColInd, + iluInfo, &iluBufSize); +#pragma GCC diagnostic pop + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseDcsrilu02_bufferSize"); + return false; + } + if (!checkCuda(cudaMalloc(&d_iluBuf, static_cast(iluBufSize)), + "cudaMalloc(d_iluBuf)")) + return false; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + st = cusparseDcsrilu02_analysis( + csHandle, iN, nnz, descrA, + d_iluVal, d_csrRowPtr, d_csrColInd, + iluInfo, CUSPARSE_SOLVE_POLICY_USE_LEVEL, d_iluBuf); +#pragma GCC diagnostic pop + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseDcsrilu02_analysis"); + return false; + } + + // ── Triangular-solve analyses (L and U, done once per pattern) ─ + // Temporary dense-vector descriptors pointing to d_svTmp (analysis only). + cusparseDnVecDescr_t vecTmp1 = nullptr, vecTmp2 = nullptr; + if (!checkCusparse(cusparseCreateDnVec(&vecTmp1, static_cast(N), + d_svTmp, CUDA_R_64F), + "cusparseCreateDnVec(tmp1)")) return false; + if (!checkCusparse(cusparseCreateDnVec(&vecTmp2, static_cast(N), + d_svTmp, CUDA_R_64F), + "cusparseCreateDnVec(tmp2)")) { + cusparseDestroyDnVec(vecTmp1); + return false; + } + + const double one = 1.0; + svBufSizeLBytes = 0; + svBufSizeUBytes = 0; + + st = cusparseSpSV_bufferSize( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatL, vecTmp1, vecTmp2, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrL, &svBufSizeLBytes); + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseSpSV_bufferSize(L)"); + cusparseDestroyDnVec(vecTmp1); cusparseDestroyDnVec(vecTmp2); + return false; + } + + st = cusparseSpSV_bufferSize( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatU, vecTmp1, vecTmp2, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrU, &svBufSizeUBytes); + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseSpSV_bufferSize(U)"); + cusparseDestroyDnVec(vecTmp1); cusparseDestroyDnVec(vecTmp2); + return false; + } + + if (!checkCuda(cudaMalloc(&d_svBufL, svBufSizeLBytes ? svBufSizeLBytes : 1), + "cudaMalloc(d_svBufL)")) { + cusparseDestroyDnVec(vecTmp1); cusparseDestroyDnVec(vecTmp2); + return false; + } + if (!checkCuda(cudaMalloc(&d_svBufU, svBufSizeUBytes ? svBufSizeUBytes : 1), + "cudaMalloc(d_svBufU)")) { + cusparseDestroyDnVec(vecTmp1); cusparseDestroyDnVec(vecTmp2); + return false; + } + + st = cusparseSpSV_analysis( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatL, vecTmp1, vecTmp2, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrL, d_svBufL); + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseSpSV_analysis(L)"); + cusparseDestroyDnVec(vecTmp1); cusparseDestroyDnVec(vecTmp2); + return false; + } + + st = cusparseSpSV_analysis( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatU, vecTmp1, vecTmp2, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrU, d_svBufU); + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseSpSV_analysis(U)"); + cusparseDestroyDnVec(vecTmp1); cusparseDestroyDnVec(vecTmp2); + return false; + } + + cusparseDestroyDnVec(vecTmp1); + cusparseDestroyDnVec(vecTmp2); + iluReady = true; + return true; + } + + bool refreshSpSVAnalysis() { + if (!activateDevice("refreshSpSVAnalysis")) + return false; + + const double one = 1.0; + const int64_t iN = static_cast(n); + + cusparseDnVecDescr_t vecTmp1 = nullptr, vecTmp2 = nullptr; + if (!checkCusparse(cusparseCreateDnVec(&vecTmp1, iN, d_svTmp, + CUDA_R_64F), + "cusparseCreateDnVec(refresh tmp1)")) + return false; + if (!checkCusparse(cusparseCreateDnVec(&vecTmp2, iN, d_svTmp, + CUDA_R_64F), + "cusparseCreateDnVec(refresh tmp2)")) { + cusparseDestroyDnVec(vecTmp1); + return false; + } + + size_t requiredL = 0, requiredU = 0; + cusparseStatus_t st = cusparseSpSV_bufferSize( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatL, vecTmp1, vecTmp2, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrL, &requiredL); + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseSpSV_bufferSize(refresh L)"); + cusparseDestroyDnVec(vecTmp1); + cusparseDestroyDnVec(vecTmp2); + return false; + } + + st = cusparseSpSV_bufferSize( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatU, vecTmp1, vecTmp2, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrU, &requiredU); + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseSpSV_bufferSize(refresh U)"); + cusparseDestroyDnVec(vecTmp1); + cusparseDestroyDnVec(vecTmp2); + return false; + } + + auto ensureBuffer = [&](void*& buffer, size_t& currentSize, + size_t requiredSize, + const char* name) -> bool { + requiredSize = requiredSize ? requiredSize : 1; + if (buffer != nullptr && currentSize >= requiredSize) + return true; + if (buffer != nullptr) { + cudaFree(buffer); + buffer = nullptr; + currentSize = 0; + } + if (!checkCuda(cudaMalloc(&buffer, requiredSize), name)) + return false; + currentSize = requiredSize; + return true; + }; + + if (!ensureBuffer(d_svBufL, svBufSizeLBytes, requiredL, + "cudaMalloc(d_svBufL refresh)") || + !ensureBuffer(d_svBufU, svBufSizeUBytes, requiredU, + "cudaMalloc(d_svBufU refresh)")) { + cusparseDestroyDnVec(vecTmp1); + cusparseDestroyDnVec(vecTmp2); + return false; + } + + st = cusparseSpSV_analysis( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatL, vecTmp1, vecTmp2, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrL, d_svBufL); + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseSpSV_analysis(refresh L)"); + cusparseDestroyDnVec(vecTmp1); + cusparseDestroyDnVec(vecTmp2); + return false; + } + + st = cusparseSpSV_analysis( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatU, vecTmp1, vecTmp2, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrU, d_svBufU); + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseSpSV_analysis(refresh U)"); + cusparseDestroyDnVec(vecTmp1); + cusparseDestroyDnVec(vecTmp2); + return false; + } + + cusparseDestroyDnVec(vecTmp1); + cusparseDestroyDnVec(vecTmp2); + return true; + } + + // ── ILU(0) fill + factorize ──────────────────────────────────────── + // Assemble CSR values from diagH + coeffH, upload to d_iluVal, then + // run in-place ILU(0) factorization. Called from gpuUploadSolverArrays + // every solve (coefficients change with pressure). + // diagH[i] = A[i,i] (diagonal) + // coeffH[fi*n+i] = off-diagonal coupling for face fi at node i + // (the SpMV computes A*v as: diag[i]*v[i] - coeff*v[nb], + // so the matrix entry is A[i,j] = -coeff[fi*n+i]) + bool fillAndFactorizeILU(const double* diagH, + const double* coeffH, + int nFacesIn) { + if (!activateDevice("fillAndFactorizeILU")) + return false; + if (!iluReady) return false; // setupCSR not yet called or previously failed + if (nFacesIn != nFaces) { + fprintf(stderr, "GpuBiCGSTABBuffers: ILU factorization face-count " + "mismatch (got %d, expected %d)\n", + nFacesIn, nFaces); + return false; + } + + const int N = static_cast(n); + // Build CSR values on CPU. + std::vector hVal(static_cast(nnz)); + for (int i = 0; i < N; ++i) { + for (int k = h_csrRowPtr[i]; k < h_csrRowPtr[i + 1]; ++k) { + const int fi = h_csrFaceIdx[k]; + if (fi < 0) { + // Diagonal entry. + hVal[k] = diagH[i]; + } else { + // Off-diagonal: A[i,j] = -coeff[fi*n+i]. + hVal[k] = -coeffH[static_cast(fi) * static_cast(N) + + static_cast(i)]; + } + } + } + if (!checkCuda(cudaMemcpy(d_iluVal, hVal.data(), + static_cast(nnz) * sizeof(double), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_iluVal values)")) + return false; + + // In-place ILU(0) factorization: d_iluVal is overwritten with L\U factors. + cusparseStatus_t st; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + st = cusparseDcsrilu02( + csHandle, N, nnz, descrA, + d_iluVal, d_csrRowPtr, d_csrColInd, + iluInfo, CUSPARSE_SOLVE_POLICY_USE_LEVEL, d_iluBuf); +#pragma GCC diagnostic pop + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseDcsrilu02"); + iluReady = false; + return false; + } + + // Check for structural zero-pivot. + int pivotPos = -1; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + st = cusparseXcsrilu02_zeroPivot(csHandle, iluInfo, &pivotPos); +#pragma GCC diagnostic pop + if (st == CUSPARSE_STATUS_ZERO_PIVOT) { + fprintf(stderr, "GpuBiCGSTABBuffers: ILU(0) zero pivot at row %d\n", + pivotPos); + iluReady = false; + return false; + } + if (st != CUSPARSE_STATUS_SUCCESS) { + checkCusparse(st, "cusparseXcsrilu02_zeroPivot"); + iluReady = false; + return false; + } + + // Re-running the triangular-solve analysis after each ILU(0) + // refactorization keeps any value-dependent SpSV descriptor state + // synchronized with d_iluVal. This is intentionally used instead of + // cusparseSpSV_updateMatrix: some installations expose that symbol in + // the header but not in the linked libcusparse. + if (!refreshSpSVAnalysis()) { + iluReady = false; + return false; + } + return true; + } + + // ── Apply ILU(0) preconditioner: sol = (LU)^{-1} * rhs ─────────── + // Two-step triangular solve: L * tmp = rhs, U * sol = tmp. + bool applyILU(const double* rhs, double* sol) const { + if (!activateDevice("applyILU")) + return false; + const double one = 1.0; + const int64_t iN = static_cast(n); + + // Temporary dense-vector descriptors for this solve call. + cusparseDnVecDescr_t vecRhs = nullptr, vecTmp = nullptr, vecSol = nullptr; + if (!checkCusparse(cusparseCreateDnVec(&vecRhs, iN, + const_cast(rhs), + CUDA_R_64F), + "cusparseCreateDnVec(rhs)")) + return false; + if (!checkCusparse(cusparseCreateDnVec(&vecTmp, iN, d_svTmp, + CUDA_R_64F), + "cusparseCreateDnVec(tmp)")) { + cusparseDestroyDnVec(vecRhs); + return false; + } + if (!checkCusparse(cusparseCreateDnVec(&vecSol, iN, sol, CUDA_R_64F), + "cusparseCreateDnVec(sol)")) { + cusparseDestroyDnVec(vecRhs); + cusparseDestroyDnVec(vecTmp); + return false; + } + + // Lower triangular solve: L * d_svTmp = rhs + if (!checkCusparse(cusparseSpSV_solve( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatL, vecRhs, vecTmp, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrL), "cusparseSpSV_solve(L)")) { + cusparseDestroyDnVec(vecRhs); + cusparseDestroyDnVec(vecTmp); + cusparseDestroyDnVec(vecSol); + return false; + } + + // Upper triangular solve: U * sol = d_svTmp + if (!checkCusparse(cusparseSpSV_solve( + csHandle, CUSPARSE_OPERATION_NON_TRANSPOSE, &one, + spMatU, vecTmp, vecSol, CUDA_R_64F, + CUSPARSE_SPSV_ALG_DEFAULT, svDescrU), "cusparseSpSV_solve(U)")) { + cusparseDestroyDnVec(vecRhs); + cusparseDestroyDnVec(vecTmp); + cusparseDestroyDnVec(vecSol); + return false; + } + + cusparseDestroyDnVec(vecRhs); + cusparseDestroyDnVec(vecTmp); + cusparseDestroyDnVec(vecSol); + return true; + } + + bool applyPreconditioner(const double* rhs, double* sol, + double diagEps) const { + if (useIlu0Preconditioner) { + if (!iluReady) + return false; + return applyILU(rhs, sol); + } + + if (!checkCuda(cudaSetDevice(deviceId), + "cudaSetDevice(applyPreconditioner)")) + return false; + const int blk = std::min(blocksFor(static_cast(n)), 65535); + jacobiKernel<<>>(rhs, d_diag, sol, diagEps, n); + return checkCuda(cudaPeekAtLastError(), "jacobiKernel"); + } +}; + +// ────────────────────── BiCGSTAB orchestration ─────────────────────────── + +// Run GPU-accelerated preconditioned BiCGSTAB. +// +// x (host, in/out) : initial guess on entry, solution on exit. +// diag_gpu must already hold d_diag and d_b, and d_nb / d_coeff must +// have been uploaded to gpu before this call. +// +// Scalars (rho, alpha, omega) stay on the CPU for numerical stability; +// only dot-product scalars and the max-abs residual are downloaded. +inline bool solveBiCGSTAB( + GpuBiCGSTABBuffers& gpu, + std::vector& x, // size n, host + double diagEps, + unsigned maxIter, + double tolerance, + unsigned& outIterations, + double& outResidual) +{ + assert(gpu.valid); + const uint32_t n = gpu.n; + const int blk = std::min(blocksFor(static_cast(n)), 65535); + + // Upload initial guess to device + if (!gpu.checkCuda(cudaMemcpy(gpu.d_x, x.data(), n * sizeof(double), + cudaMemcpyHostToDevice), + "cudaMemcpy(d_x initial)")) + return false; + // Zero all work vectors. cudaMalloc does not zero GPU memory, and d_z in + // particular is read (multiplied by omega=0) on the early-convergence path + // before it has been written by jacobiKernel β€” giving 0*NaN = NaN if the + // GPU heap happened to contain NaN from a prior allocation. + if (!gpu.checkCuda(cudaMemset(gpu.d_p, 0, n * sizeof(double)), + "cudaMemset(d_p)")) return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_v, 0, n * sizeof(double)), + "cudaMemset(d_v)")) return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_y, 0, n * sizeof(double)), + "cudaMemset(d_y)")) return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_z, 0, n * sizeof(double)), + "cudaMemset(d_z)")) return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_s, 0, n * sizeof(double)), + "cudaMemset(d_s)")) return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_t, 0, n * sizeof(double)), + "cudaMemset(d_t)")) return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_Ax, 0, n * sizeof(double)), + "cudaMemset(d_Ax)")) return false; + + // r0 = b - A*x0 + if (!gpu.spmv(gpu.d_x, gpu.d_Ax)) + return false; + residualKernel<<>>(gpu.d_b, gpu.d_Ax, gpu.d_r, n); + if (!gpu.checkCuda(cudaPeekAtLastError(), "residualKernel")) + return false; + // r_hat = r0 (shadow residual; never changes) + if (!gpu.checkCuda(cudaMemcpy(gpu.d_rhat, gpu.d_r, n * sizeof(double), + cudaMemcpyDeviceToDevice), + "cudaMemcpy(d_rhat initial)")) + return false; + + // Normalise both convergence checks by ||b||_inf, matching the CPU path which + // uses `residual < tolerance * b_norm`. Without this the GPU checks the + // raw (unnormalised) residual, which is 10–1000Γ— tighter than the CPU + // threshold. BiCGSTAB then keeps iterating on an already-converged system + // until breakdown (ρ β†’ 0 β†’ Ξ± = NaN) poisons the solution. + // Both the s-check (early exit) and r-check (end-of-loop) use eff_tol. + const double b_norm = gpu.maxAbs(gpu.d_b); + if (!std::isfinite(b_norm) || !std::isfinite(tolerance) || tolerance <= 0.f) + return false; + const double eff_tol = (b_norm > 1e-100) ? tolerance * b_norm : tolerance; + if (!std::isfinite(eff_tol) || eff_tol <= 0.f) + return false; + + double rho = 1.0; + double alpha = 1.0; + double omega = 1.0; + bool converged = false; + outIterations = 0; + outResidual = 0.0; + + // Restart counter: limits breakdown-restarts to avoid infinite loops. + int restartCount = 0; + static constexpr int kMaxRestarts = 3; + int trueResidualRestartCount = 0; + static constexpr int kMaxTrueResidualRestarts = 8; + + auto recomputeTrueResidual = [&]() -> bool { + if (!gpu.spmv(gpu.d_x, gpu.d_Ax)) + return false; + residualKernel<<>>(gpu.d_b, gpu.d_Ax, + gpu.d_r, n); + if (!gpu.checkCuda(cudaPeekAtLastError(), "residualKernel(true)")) + return false; + outResidual = static_cast(gpu.maxAbs(gpu.d_r)); + return std::isfinite(outResidual); + }; + + auto restartFromCurrentResidual = [&](const char* label) -> bool { + if (!gpu.checkCuda(cudaMemcpy(gpu.d_rhat, gpu.d_r, n * sizeof(double), + cudaMemcpyDeviceToDevice), + label)) + return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_p, 0, n * sizeof(double)), + "cudaMemset(d_p residual restart)")) + return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_v, 0, n * sizeof(double)), + "cudaMemset(d_v residual restart)")) + return false; + rho = 1.0; + alpha = 1.0; + omega = 1.0; + return true; + }; + + outResidual = static_cast(gpu.maxAbs(gpu.d_r)); + if (!std::isfinite(outResidual)) + return false; + if (outResidual < eff_tol) + return std::all_of(x.begin(), x.end(), + [](double value) { return std::isfinite(value); }); + + for (unsigned iter = 0; iter < maxIter; ++iter) { + + // rho_new = + const double rho_new = + static_cast(gpu.dotProduct(gpu.d_rhat, gpu.d_r)); + if (!std::isfinite(rho_new)) + return false; + if (std::abs(rho_new) < 1e-100) { + // ρ-breakdown: restart from current r. + if (restartCount >= kMaxRestarts) + return false; + ++restartCount; + // Reset shadow residual to current r and reinitialise scalars. + if (!gpu.checkCuda(cudaMemcpy(gpu.d_rhat, gpu.d_r, n * sizeof(double), + cudaMemcpyDeviceToDevice), + "cudaMemcpy(d_rhat rho restart)")) + return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_p, 0, n * sizeof(double)), + "cudaMemset(d_p rho restart)")) + return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_v, 0, n * sizeof(double)), + "cudaMemset(d_v rho restart)")) + return false; + rho = 1.0; alpha = 1.0; omega = 1.0; + continue; + } + if (!std::isfinite(rho) || !std::isfinite(alpha) || + !std::isfinite(omega) || std::abs(omega) < 1e-100) + return false; + + // p = r + beta*(p - omega*v), beta = (rho_new/rho)*(alpha/omega) + const double betaD = (rho_new / rho) * (alpha / omega); + if (!std::isfinite(betaD)) + return false; + const double beta = betaD; + const double beta_omega = betaD * omega; + if (!std::isfinite(beta) || !std::isfinite(beta_omega)) + return false; + rho = rho_new; + updatePKernel<<>>( + gpu.d_r, beta, beta_omega, gpu.d_v, gpu.d_p, n); + if (!gpu.checkCuda(cudaPeekAtLastError(), "updatePKernel")) + return false; + + // y = M^{-1} p + if (!gpu.applyPreconditioner(gpu.d_p, gpu.d_y, diagEps)) + return false; + + // v = A y + if (!gpu.spmv(gpu.d_y, gpu.d_v)) + return false; + + // alpha = rho_new / + const double r_hat_v = + static_cast(gpu.dotProduct(gpu.d_rhat, gpu.d_v)); + if (!std::isfinite(r_hat_v)) + return false; + if (std::abs(r_hat_v) < 1e-100) { + // Lanczos breakdown: restart from current r. + if (restartCount >= kMaxRestarts) + return false; + ++restartCount; + if (!gpu.checkCuda(cudaMemcpy(gpu.d_rhat, gpu.d_r, n * sizeof(double), + cudaMemcpyDeviceToDevice), + "cudaMemcpy(d_rhat lanczos restart)")) + return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_p, 0, n * sizeof(double)), + "cudaMemset(d_p lanczos restart)")) + return false; + if (!gpu.checkCuda(cudaMemset(gpu.d_v, 0, n * sizeof(double)), + "cudaMemset(d_v lanczos restart)")) + return false; + rho = 1.0; alpha = 1.0; omega = 1.0; + continue; + } + alpha = rho_new / r_hat_v; + if (!std::isfinite(alpha)) + return false; + + // s = r - alpha*v + subtractScaledKernel<<>>( + gpu.d_r, alpha, gpu.d_v, gpu.d_s, n); + if (!gpu.checkCuda(cudaPeekAtLastError(), "subtractScaledKernel")) + return false; + + // Check convergence on s + outResidual = static_cast(gpu.maxAbs(gpu.d_s)); + if (!std::isfinite(outResidual)) + return false; + if (outResidual < eff_tol) { + // x += alpha*y (z term is zero since we converged early) + updateXKernel<<>>( + gpu.d_x, alpha, gpu.d_y, + 0.0, gpu.d_z, n); + if (!gpu.checkCuda(cudaPeekAtLastError(), "updateXKernel(early)")) + return false; + ++outIterations; + if (!recomputeTrueResidual()) + return false; + if (outResidual < eff_tol) { + converged = true; + break; + } + if (trueResidualRestartCount >= kMaxTrueResidualRestarts) + return false; + ++trueResidualRestartCount; + if (!restartFromCurrentResidual( + "cudaMemcpy(d_rhat s true-residual restart)")) + return false; + continue; + } + + // z = M^{-1} s + if (!gpu.applyPreconditioner(gpu.d_s, gpu.d_z, diagEps)) + return false; + + // t = A z + if (!gpu.spmv(gpu.d_z, gpu.d_t)) + return false; + + // omega = / + const double t_s = static_cast(gpu.dotProduct(gpu.d_t, gpu.d_s)); + const double t_t = static_cast(gpu.dotProduct(gpu.d_t, gpu.d_t)); + if (!std::isfinite(t_s) || !std::isfinite(t_t) || t_t <= 1e-100) + return false; + omega = t_s / t_t; + if (!std::isfinite(omega) || std::abs(omega) < 1e-100) + return false; + + // x += alpha*y + omega*z + updateXKernel<<>>( + gpu.d_x, alpha, gpu.d_y, omega, gpu.d_z, n); + if (!gpu.checkCuda(cudaPeekAtLastError(), "updateXKernel")) + return false; + + // r = s - omega*t + updateRKernel<<>>( + gpu.d_s, omega, gpu.d_t, gpu.d_r, n); + if (!gpu.checkCuda(cudaPeekAtLastError(), "updateRKernel")) + return false; + + // Check convergence on r + outResidual = static_cast(gpu.maxAbs(gpu.d_r)); + if (!std::isfinite(outResidual)) + return false; + ++outIterations; + if (outResidual < eff_tol) { + if (!recomputeTrueResidual()) + return false; + if (outResidual < eff_tol) { + converged = true; + break; + } + if (trueResidualRestartCount >= kMaxTrueResidualRestarts) + return false; + ++trueResidualRestartCount; + if (!restartFromCurrentResidual( + "cudaMemcpy(d_rhat true-residual restart)")) + return false; + continue; + } + } + + if (!converged) + return false; + + // Download solution to host + if (!gpu.checkCuda(cudaMemcpy(x.data(), gpu.d_x, n * sizeof(double), + cudaMemcpyDeviceToHost), + "cudaMemcpy(d_x final)")) + return false; + return std::all_of(x.begin(), x.end(), + [](double value) { return std::isfinite(value); }); +} + +} // namespace gpu +} // namespace viennals + +#endif // VIENNALS_GPU_BICGSTAB diff --git a/include/viennals/lsOxidationBiCGSTABInterface.hpp b/include/viennals/lsOxidationBiCGSTABInterface.hpp new file mode 100644 index 00000000..7c892196 --- /dev/null +++ b/include/viennals/lsOxidationBiCGSTABInterface.hpp @@ -0,0 +1,77 @@ +// C++ (g++) interface to the GPU BiCGSTAB solver. +// +// This header is safe to include from any .cpp file compiled by g++. +// It only forward-declares GpuBiCGSTABBuffers (opaque handle) and +// declares free functions that are implemented in +// ViennaLS_GPU (lsOxidationBiCGSTABKernels.cu, compiled by nvcc). +// +// The actual CUDA kernels live in lsOxidationBiCGSTAB.cuh. That file +// must never be included from a .cpp compiled by g++ β€” only from .cu files. + +#pragma once + +#ifdef VIENNALS_GPU_BICGSTAB + +#include +#include + +namespace viennals { +namespace gpu { + +// Sentinel value used in the neighbor-ID array to mark boundary / out-of-bounds +// faces (must match the constant in lsOxidationBiCGSTAB.cuh). +static constexpr uint32_t kNoNode = 0xFFFFFFFFu; + +// Opaque handle β€” complete definition is in lsOxidationBiCGSTAB.cuh / +// lsOxidationBiCGSTABKernels.cu. Consumers hold a raw pointer only. +struct GpuBiCGSTABBuffers; + +// Allocate GPU buffers for a solver with `n` nodes and `nFaces` (2*D) faces. +// Returns nullptr if CUDA is unavailable. +GpuBiCGSTABBuffers *allocGpuBuffers(uint32_t n, int nFaces, + bool useIlu0Preconditioner); + +// Free previously allocated GPU buffers. Safe to call with nullptr. +void freeGpuBuffers(GpuBiCGSTABBuffers *gpu); + +// Human-readable detail for the last GPU wrapper failure on this thread. +const char *gpuGetLastErrorMessage(); + +// Is the buffer handle valid (non-null and successfully allocated)? +bool gpuIsValid(const GpuBiCGSTABBuffers *gpu); + +// Upload geometry-fixed neighbor-ID array (face-major, kNoNode = 0xFFFFFFFF). +// `count` must equal nFaces * n. +bool gpuUploadNeighborIds(GpuBiCGSTABBuffers *gpu, const uint32_t *nb, + std::size_t count); + +// Build the CSR sparsity pattern from h_nb (face-major, length nFaces*n), +// upload to the device, and run CUSPARSE symbolic analysis for ILU(0) and +// the two triangular solves. Must be called after gpuUploadNeighborIds and +// before the first gpuUploadSolverArrays / gpuSolveBiCGSTAB call. +bool gpuSetupCSR(GpuBiCGSTABBuffers *gpu, const uint32_t *h_nb, uint32_t n, + int nFaces); + +// Upload per-solve arrays (diag, b, faceCoeffs) and re-factorize ILU(0). +// `diagLen` == n, `coeffLen` == nFaces * n. +bool gpuUploadSolverArrays(GpuBiCGSTABBuffers *gpu, const double *diag, + const double *b, const double *coeff, + uint32_t diagLen, std::size_t coeffLen); + +// Upload only the RHS vector (d_b). Use when the matrix geometry is already +// uploaded and only the right-hand side changes (e.g. successive Stokes +// component solves that share the same stiffness matrix). +bool gpuUploadRhs(GpuBiCGSTABBuffers *gpu, const double *b, uint32_t n); + +// Run GPU BiCGSTAB. +// x (length n, host): initial guess on entry, solution on exit. +// outResidual is the raw (unnormalized) max-abs residual on exit. +// Returns true only when the GPU solve converged and produced finite values. +bool gpuSolveBiCGSTAB(GpuBiCGSTABBuffers *gpu, double *x, double diagEps, + unsigned maxIter, double tolerance, + unsigned &outIterations, double &outResidual); + +} // namespace gpu +} // namespace viennals + +#endif // VIENNALS_GPU_BICGSTAB diff --git a/include/viennals/lsOxidationDeformation.hpp b/include/viennals/lsOxidationDeformation.hpp new file mode 100644 index 00000000..a4e49d4d --- /dev/null +++ b/include/viennals/lsOxidationDeformation.hpp @@ -0,0 +1,3247 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace viennals { + +/// Parameters for the Cartesian-grid oxide deformation model. +struct OxidationDeformationParameters { + double viscosity = 1.; + double bulkModulus = 1.; + double ambientPressure = 0.; + double pressureTolerance = 1e-8; + double minMechanicsBoundaryDistance = 0.05; + double shearModulus = 0.; + double stressRelaxationTime = 0.; + double stressTimeStep = 1.; + unsigned harmonicIterations = 500; + unsigned mechanicsIterations = 5; + unsigned pressureIterations = 10000; + unsigned stokesIterations = 200; + double mechanicsTolerance = 1e-8; + double stokesTolerance = 1e-8; + double tolerance = 1e-8; + double relaxation = 0.7; // SIMPLE velocity under-relaxation (0 < Ξ± ≀ 1) + double pressureRelaxation = + 0.5; // SIMPLE pressure under-relaxation (0 < Ξ² ≀ 1) + std::size_t maxGridPoints = 5000000; + int material = -1; +}; + +/// Propagates the volume expansion generated at the Si/SiO2 interface through +/// the oxide as a Cartesian-grid deformation velocity field. +/// +/// This class implements the deformation part of the oxidation workflow in the +/// same fixed-grid spirit as the diffusion solver above. The Si/SiO2 boundary +/// is driven by the volume expansion velocity +/// +/// (gamma - 1) / gamma * k C / N +/// +/// along the reaction-interface normal. The initial velocity field in the oxide +/// is obtained from a component-wise harmonic extension and is then relaxed +/// with a Cartesian-grid quasi-static Stokes solve. The mechanical update +/// solves a pressure equation from the current velocity divergence and a +/// velocity momentum equation, including pressure-gradient and viscoelastic +/// deviatoric stress terms. The Si/SiO2 interface uses the oxidation expansion +/// velocity, the oxide/ambient interface uses a traction-free boundary, and +/// optional mask contacts use the mask velocity field. +template +class OxidationDeformation final : public VelocityField, + public OxidationSolverBase { + using IndexType = viennahrle::Index; + using ConstSparseIterator = + viennahrle::ConstSparseIterator::DomainType>; + +private: + // bring base members into scope + using OxidationSolverBase::noNode; + using OxidationSolverBase::nodeLookupFlat; + using OxidationSolverBase::initNodeLookup; + using OxidationSolverBase::lookupNode; + using OxidationSolverBase::minIndex; + using OxidationSolverBase::maxIndex; + using OxidationSolverBase::extents; + using OxidationSolverBase::strides; + using OxidationSolverBase::gridDelta; + using OxidationSolverBase::crosses; + using OxidationSolverBase::valueAt; + using OxidationSolverBase::inBounds; + using OxidationSolverBase::linearIndex; + using OxidationSolverBase::increment; + using OxidationSolverBase::findNearbyNode; + using OxidationSolverBase::initializeGridFromInterfaces; + + enum class Boundary { NONE, REACTION, AMBIENT, MASK }; + + struct BoundaryIntersection { + Boundary boundary = Boundary::NONE; + T distance = 0.; + }; + + template struct StencilPoint { + ValueType value{}; + T distance = 1.; + }; + + struct Node { + IndexType index; + Vec3D velocity{0., 0., 0.}; + T pressure = 0.; + T strainTrace = 0.; + std::array strainRateTensor{}; + std::array stressTensor{}; + T vonMisesStress = 0.; + }; + + SmartPointer> reactionInterface = nullptr; + SmartPointer> ambientInterface = nullptr; + SmartPointer> maskInterface = nullptr; + SmartPointer> diffusionField = nullptr; + SmartPointer> maskVelocityField = nullptr; + OxidationDeformationParameters deformationParameters; + OxidationParameters oxidationParameters; + int reactionSign = 1; + int ambientSign = -1; + int maskSign = 1; + + IndexType requestedMinIndex{}; + IndexType requestedMaxIndex{}; + unsigned iterations = 0; + T residual = std::numeric_limits::max(); + // Last achieved iteration counts and residuals for pressure and Stokes + // solves. + unsigned lastPressureIters_ = 0; + T lastPressureResidual_ = 0.; + unsigned lastStokesIters_ = 0; + T lastStokesResidual_ = 0.; + T avgExpansionSpeed_ = 0.; + bool avgExpansionSpeedComputed = false; + bool solved = false; + bool nodesDirty_ = true; + std::array maxVelocity_{}; + bool useRequestedBounds = false; + std::unordered_map, detail::IndexTypeHasher> + deviatoricStressHistory; + + // Warm-start storage: solutions from previous time step used as initial guess + std::vector> previousVelocity_; + std::vector previousPressure_; + bool hasPreviousSolution_ = false; + + static bool isFiniteVec(const Vec3D &value) { + for (unsigned i = 0; i < 3; ++i) + if (!std::isfinite(value[i])) + return false; + return true; + } + + static bool isFiniteTensor(const std::array &value) { + for (const auto component : value) + if (!std::isfinite(component)) + return false; + return true; + } + + // Face-major flat BC arrays: index = fi * n + nodeId, fi in [0, 2*D). + std::vector faceBCTypes_; + std::vector faceBCDists_; + std::vector + touchesAmbient_; // 1 if node touches the ambient (free) surface + + // GPU solver selection. Semantics match OxidationDiffusion. + GpuMode gpuMode_ = GpuMode::Cpu; + GpuPreconditioner gpuPreconditioner_ = GpuPreconditioner::Jacobi; + mutable std::string + lastLoggedBackend_; // suppresses repeated "using X" messages + +#ifdef VIENNALS_GPU_BICGSTAB + // Geometry-fixed (per buildNodes()) arrays for GPU pressure solve. + // Layout matches pressCoeff/pressNeighId in solvePressure() so the + // same spmvKernel can be reused without any CPU-side reformatting. + std::vector pressCoeffGpu_; // face-major [2D * n] + std::vector pressNeighId32_; // face-major [2D * n] + std::vector actualDiagGpu_; // [n], effective GPU matrix diagonal + gpu::GpuBiCGSTABBuffers *gpuPressBufs_ = nullptr; + + // Geometry-fixed arrays for GPU Stokes velocity solve. The diagonal is + // component-major because mixed MASK contact is Dirichlet in the normal + // component and Neumann/self-canceling in tangential components. + std::vector stokesCoeffGpu_; // face-major [2D * n] + std::vector stokesNeighId32_; // face-major [2D * n] + std::vector stokesDiagGpu_; // component-major [D * n] + gpu::GpuBiCGSTABBuffers *gpuStokesBufs_ = nullptr; + + // Harmonic velocity solver GPU arrays. The neighbor IDs are identical to + // Stokes (stokesNeighId32_ is reused); only the coefficients and diagonal + // differ (all interior coefficients = 1.0, diagonal = interior-face count). + std::vector + harmonicCoeffGpu_; // face-major [2D * n], 1.0 for interior + std::vector + harmonicDiagGpu_; // [n], = count of interior faces per node + gpu::GpuBiCGSTABBuffers *gpuHarmonicBufs_ = nullptr; +#endif + +public: + std::vector nodes; + + OxidationDeformation() = default; + + OxidationDeformation( + SmartPointer> passedReactionInterface, + SmartPointer> passedAmbientInterface, + SmartPointer> passedDiffusionField, + OxidationParameters passedOxidationParameters, + OxidationDeformationParameters passedDeformationParameters = {}) + : reactionInterface(passedReactionInterface), + ambientInterface(passedAmbientInterface), + diffusionField(passedDiffusionField), + deformationParameters(passedDeformationParameters), + oxidationParameters(passedOxidationParameters) {} + + template static auto New(Args &&...args) { + return SmartPointer::New(std::forward(args)...); + } + + ~OxidationDeformation() { +#ifdef VIENNALS_GPU_BICGSTAB + gpu::freeGpuBuffers(gpuPressBufs_); + gpu::freeGpuBuffers(gpuStokesBufs_); + gpu::freeGpuBuffers(gpuHarmonicBufs_); +#endif + } + + void setGpuMode(GpuMode mode) { gpuMode_ = mode; } + void setGpuPreconditioner(GpuPreconditioner prec) { + gpuPreconditioner_ = prec; + } + + void setReactionInterface(SmartPointer> passedInterface) { + reactionInterface = passedInterface; + nodesDirty_ = true; + solved = false; + } + + void setAmbientInterface(SmartPointer> passedInterface) { + ambientInterface = passedInterface; + nodesDirty_ = true; + solved = false; + } + + void setMaskInterface(SmartPointer> passedInterface, + int passedMaskSign = 1) { + maskInterface = passedInterface; + maskSign = (passedMaskSign < 0) ? -1 : 1; + nodesDirty_ = true; + solved = false; + } + + void clearMaskInterface() { + maskInterface = nullptr; + nodesDirty_ = true; + solved = false; + } + + void + setMaskVelocityField(SmartPointer> passedVelocityField) { + maskVelocityField = passedVelocityField; + solved = false; + } + + void clearMaskVelocityField() { + maskVelocityField = nullptr; + solved = false; + } + + void setDiffusionField( + SmartPointer> passedDiffusionField) { + diffusionField = passedDiffusionField; + solved = false; + } + + void setOxidationParameters(OxidationParameters passedParameters) { + oxidationParameters = passedParameters; + solved = false; + } + + void + setDeformationParameters(OxidationDeformationParameters passedParameters) { + deformationParameters = passedParameters; + solved = false; + } + + void setOxideSigns(int passedReactionSign, int passedAmbientSign) { + reactionSign = (passedReactionSign < 0) ? -1 : 1; + ambientSign = (passedAmbientSign < 0) ? -1 : 1; + solved = false; + } + + void setSolveBounds(const IndexType &passedMinIndex, + const IndexType &passedMaxIndex) { + requestedMinIndex = passedMinIndex; + requestedMaxIndex = passedMaxIndex; + useRequestedBounds = true; + nodesDirty_ = true; + solved = false; + } + + void clearSolveBounds() { + useRequestedBounds = false; + nodesDirty_ = true; + solved = false; + } + + void markGeometryChanged() { + nodesDirty_ = true; + solved = false; + } + + void apply() { + if (reactionInterface == nullptr || ambientInterface == nullptr || + diffusionField == nullptr) { + Logger::getInstance() + .addError("OxidationDeformation: Missing interface or " + "diffusion field.") + .print(); + return; + } + + if (nodesDirty_) { + if (!initialiseGrid()) + return; // base class already logged the error + buildNodes(); + nodesDirty_ = false; + if (nodes.empty()) + Logger::getInstance() + .addWarning("OxidationDeformation: no oxide nodes found after " + "buildNodes(). Verify that the reaction and ambient " + "level sets enclose a non-empty oxide band.") + .print(); + hasPreviousSolution_ = + false; // Geometry changed, invalidate in-memory warm-start + // Try restoring velocity, pressure, and stress history from level set + // pointData (written by writeFieldsToLevelSet() before the previous + // advection and remapped+filled by lsAdvect + lsInterior). + seedFromLevelSet(); + } else if (hasPreviousSolution_ && + previousVelocity_.size() == nodes.size() && + previousPressure_.size() == nodes.size()) { + // Warm-start: restore previous solution as initial guess for solver. + // Geometry stability check: only warm-start if node count matches + // (prevents using stale solutions after grid refinement/coarsening). This + // typically reduces solver iterations by 30-50% since the previous step's + // solution is close to the new one. + for (std::size_t i = 0; i < nodes.size(); ++i) { + nodes[i].velocity = previousVelocity_[i]; + nodes[i].pressure = previousPressure_[i]; + } + } + + const std::size_t nn = nodes.size(); + Timer<> tHarmonic, tMechanics; + tHarmonic.start(); + solveVelocity(); + tHarmonic.finish(); + tMechanics.start(); + solveMechanics(); + tMechanics.finish(); + Logger::getInstance() + .addTiming(" deformation n=" + std::to_string(nn) + " harmonic", + tHarmonic) + .addTiming(" deformation n=" + std::to_string(nn) + + " mechanics-total", + tMechanics) + .print(); + avgExpansionSpeedComputed = false; + + maxVelocity_.fill(T(0)); + for (const auto &node : nodes) { + for (unsigned d = 0; d < D; ++d) + maxVelocity_[d] = std::max(maxVelocity_[d], std::abs(node.velocity[d])); + } + const auto unresolvedMax = estimateMaxUnresolvedAmbientVelocity(); + for (unsigned d = 0; d < D; ++d) + maxVelocity_[d] = std::max(maxVelocity_[d], unresolvedMax[d]); + + // Save current solution for warm-start on next apply() + previousVelocity_.resize(nodes.size()); + previousPressure_.resize(nodes.size()); + for (std::size_t i = 0; i < nodes.size(); ++i) { + previousVelocity_[i] = nodes[i].velocity; + previousPressure_[i] = nodes[i].pressure; + } + hasPreviousSolution_ = true; + + solved = true; + } + + Vec3D getVectorVelocity(const Vec3D &coordinate, int material, + const Vec3D & /*normalVector*/, + unsigned long /*pointId*/) final { + if (!solved) + apply(); + + if (deformationParameters.material >= 0 && + material != deformationParameters.material) + return {0., 0., 0.}; + + const auto velocity = getVelocity(coordinate); + T norm2 = 0.; + for (unsigned d = 0; d < D; ++d) + norm2 += velocity[d] * velocity[d]; + if (norm2 > std::numeric_limits::epsilon()) + return velocity; + + return unresolvedAmbientVelocity(coordinate); + } + + T getScalarVelocity(const Vec3D &coordinate, int material, + const Vec3D &normalVector, + unsigned long /*pointId*/) final { + return 0.; + } + + T getDissipationAlpha(int direction, int material, + const Vec3D & /*centralDifferences*/) final { + if (deformationParameters.material >= 0 && + material != deformationParameters.material) + return 0.; + return maxVelocity_[direction]; + } + +private: + template + ValueType getField(const IndexType &index, ValueType fallback, + NodeAccessor accessor) const { + const std::size_t nodeId = lookupNode(index); + if (nodeId != noNode) + return accessor(nodes[nodeId]); + + const auto nearby = findNearbyNode(index); + if (nearby == noNode) + return fallback; + return accessor(nodes[nearby]); + } + + template + ValueType getField(const Vec3D &coordinate, ValueType fallback, + NodeAccessor accessor) const { + // D-linear interpolation for Vec3D (velocity) and scalar T (pressure). + // These are the types queried during level-set advection; interpolating at + // the exact interface coordinate eliminates the O(gridDelta) nearest-node + // error that caused the SiO2/mask interface to shift with grid delta. + // Other types (e.g. stress tensor std::array) use nearest-node because + // they lack scalar multiply and are not used for advection. + if constexpr (!std::is_same_v> && + !std::is_same_v) { + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + return getField(index, fallback, accessor); + } else { + using IdxScalar = std::decay_t()[0])>; + IndexType lo; + T frac[D]; + for (unsigned d = 0; d < D; ++d) { + const T c = coordinate[d] / gridDelta; + const T c_flo = std::floor(c); + lo[d] = static_cast(c_flo); + frac[d] = c - c_flo; + } + + ValueType result{}; + T totalWeight = T(0); + for (int corner = 0; corner < (1 << D); ++corner) { + IndexType idx = lo; + T w = T(1); + for (unsigned d = 0; d < D; ++d) { + if ((corner >> d) & 1) { + idx[d]++; + w *= frac[d]; + } else { + w *= T(1) - frac[d]; + } + } + if (w < T(1e-14)) + continue; + const std::size_t nodeId = lookupNode(idx); + if (nodeId == noNode) + continue; + result = result + accessor(nodes[nodeId]) * w; + totalWeight += w; + } + + if (totalWeight < T(1e-14)) + return fallback; + if (totalWeight < T(1) - T(1e-6)) + result = result * (T(1) / totalWeight); + return result; + } + } + +public: + Vec3D getVelocity(const Vec3D &coordinate) const { + return getField(coordinate, Vec3D{0., 0., 0.}, + [](const Node &n) { return n.velocity; }); + } + + Vec3D getVelocity(const IndexType &index) const { + return getField(index, Vec3D{0., 0., 0.}, + [](const Node &n) { return n.velocity; }); + } + + T getPressure(const Vec3D &coordinate) const { + return getField(coordinate, T(0), [](const Node &n) { return n.pressure; }); + } + + T getPressure(const IndexType &index) const { + return getField(index, T(0), [](const Node &n) { return n.pressure; }); + } + + T getStrainTrace(const Vec3D &coordinate) const { + return getField(coordinate, T(0), + [](const Node &n) { return n.strainTrace; }); + } + + T getStrainTrace(const IndexType &index) const { + return getField(index, T(0), [](const Node &n) { return n.strainTrace; }); + } + + std::array getStrainRateTensor(const Vec3D &coordinate) const { + return getField(coordinate, std::array{}, + [](const Node &n) { return n.strainRateTensor; }); + } + + std::array getStrainRateTensor(const IndexType &index) const { + return getField(index, std::array{}, + [](const Node &n) { return n.strainRateTensor; }); + } + + std::array getStressTensor(const Vec3D &coordinate) const { + return getField(coordinate, std::array{}, + [](const Node &n) { return n.stressTensor; }); + } + + std::array getStressTensor(const IndexType &index) const { + return getField(index, std::array{}, + [](const Node &n) { return n.stressTensor; }); + } + + T getVonMisesStress(const Vec3D &coordinate) const { + return getField(coordinate, T(0), + [](const Node &n) { return n.vonMisesStress; }); + } + + T getVonMisesStress(const IndexType &index) const { + return getField(index, T(0), + [](const Node &n) { return n.vonMisesStress; }); + } + + unsigned getIterations() const { return iterations; } + T getResidual() const { return residual; } + T getLastPressureResidual() const { return lastPressureResidual_; } + T getLastStokesResidual() const { return lastStokesResidual_; } + bool lastSolveConverged() const { + return std::isfinite(residual) && + residual <= deformationParameters.mechanicsTolerance && + std::isfinite(lastPressureResidual_) && + lastPressureResidual_ <= deformationParameters.pressureTolerance && + std::isfinite(lastStokesResidual_) && + lastStokesResidual_ <= deformationParameters.stokesTolerance && + hasFiniteSolution(); + } + bool hasFiniteSolution() const { + for (const auto &node : nodes) { + if (!isFiniteVec(node.velocity) || !std::isfinite(node.pressure) || + !std::isfinite(node.strainTrace) || + !isFiniteTensor(node.strainRateTensor) || + !isFiniteTensor(node.stressTensor) || + !std::isfinite(node.vonMisesStress)) + return false; + } + return true; + } + std::size_t getNumberOfSolutionNodes() const { return nodes.size(); } + T avgExpansionSpeed() { + if (!avgExpansionSpeedComputed) { + computeAvgExpansionSpeed(); + avgExpansionSpeedComputed = true; + } + return avgExpansionSpeed_; + } + template void forEachSolutionNode(Callback callback) const { + for (const auto &node : nodes) + callback(node.index, node.pressure); + } + + /// Write velocity (Vec3D) and viscoelastic stress history (3 tensor-row + /// vectors) into ambientInterface->getPointData(). Called alongside + /// OxidationDiffusion::writePersistentFields() so a single lsInterior pass + /// fills the oxide interior with all warm-start data. + void writeFieldsToLevelSet() { + if (nodes.empty() || ambientInterface == nullptr) + return; + + using VD = typename PointData::VectorDataType; + VD velocity, stressR0, stressR1, stressR2; + + ConstSparseIterator it(ambientInterface->getDomain()); + for (; !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + const IndexType idx = it.getStartIndices(); + const std::size_t nId = lookupNode(idx); + + if (nId != noNode) { + const auto &n = nodes[nId]; + velocity.push_back( + isFiniteVec(n.velocity) ? n.velocity : Vec3D{T(0), T(0), T(0)}); + const auto sIt = deviatoricStressHistory.find(idx); + if (sIt != deviatoricStressHistory.end() && + isFiniteTensor(sIt->second)) { + const auto &s = sIt->second; + stressR0.push_back({s[0], s[1], s[2]}); + stressR1.push_back({s[3], s[4], s[5]}); + stressR2.push_back({s[6], s[7], s[8]}); + } else { + stressR0.push_back({T(0), T(0), T(0)}); + stressR1.push_back({T(0), T(0), T(0)}); + stressR2.push_back({T(0), T(0), T(0)}); + } + } else { + velocity.push_back({T(0), T(0), T(0)}); + stressR0.push_back({T(0), T(0), T(0)}); + stressR1.push_back({T(0), T(0), T(0)}); + stressR2.push_back({T(0), T(0), T(0)}); + } + } + + auto &pd = ambientInterface->getPointData(); + pd.insertReplaceVectorData(std::move(velocity), "OxVelocity"); + pd.insertReplaceVectorData(std::move(stressR0), "OxStressR0"); + pd.insertReplaceVectorData(std::move(stressR1), "OxStressR1"); + pd.insertReplaceVectorData(std::move(stressR2), "OxStressR2"); + } + +private: + /// Reads velocity, pressure (shared with diffusion's "OxPressure"), and + /// stress history from ambientInterface pointData into the warm-start state. + /// Called in apply() after buildNodes() when hasPreviousSolution_=false. + void seedFromLevelSet() { + if (ambientInterface == nullptr || nodes.empty()) + return; + + auto &pd = ambientInterface->getPointData(); + const int vIdx = pd.getVectorDataIndex("OxVelocity"); + const int r0Idx = pd.getVectorDataIndex("OxStressR0"); + const int r1Idx = pd.getVectorDataIndex("OxStressR1"); + const int r2Idx = pd.getVectorDataIndex("OxStressR2"); + const int pIdx = pd.getScalarDataIndex("OxPressure"); + + const bool hasVelocity = (vIdx != -1); + const bool hasStress = (r0Idx != -1 && r1Idx != -1 && r2Idx != -1); + const bool hasPressure = (pIdx != -1); + + if (!hasVelocity && !hasStress && !hasPressure) + return; + + const auto *vd = hasVelocity ? pd.getVectorData(vIdx) : nullptr; + const auto *r0d = hasStress ? pd.getVectorData(r0Idx) : nullptr; + const auto *r1d = hasStress ? pd.getVectorData(r1Idx) : nullptr; + const auto *r2d = hasStress ? pd.getVectorData(r2Idx) : nullptr; + const auto *ppd = hasPressure ? pd.getScalarData(pIdx) : nullptr; + + previousVelocity_.assign(nodes.size(), Vec3D{}); + previousPressure_.assign(nodes.size(), T(0)); + + ConstSparseIterator it(ambientInterface->getDomain()); + for (; !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + const auto ptId = it.getPointId(); + const IndexType idx = it.getStartIndices(); + const std::size_t ni = lookupNode(idx); + + if (ni != noNode) { + if (vd && ptId < static_cast(vd->size()) && + isFiniteVec((*vd)[ptId])) + previousVelocity_[ni] = (*vd)[ptId]; + if (ppd && ptId < static_cast(ppd->size()) && + std::isfinite((*ppd)[ptId])) + previousPressure_[ni] = (*ppd)[ptId]; + } + + if (hasStress && ptId < static_cast(r0d->size()) && + ptId < static_cast(r1d->size()) && + ptId < static_cast(r2d->size())) { + const auto &row0 = (*r0d)[ptId]; + const auto &row1 = (*r1d)[ptId]; + const auto &row2 = (*r2d)[ptId]; + std::array s{row0[0], row0[1], row0[2], row1[0], row1[1], + row1[2], row2[0], row2[1], row2[2]}; + if (isFiniteTensor(s)) + deviatoricStressHistory[idx] = s; + } + } + + if (hasVelocity || hasPressure) { + hasPreviousSolution_ = true; + // Apply the restored state immediately to the current nodes so + // the Stokes solve warm-starts on this (nodesDirty_=true) apply() call. + for (std::size_t i = 0; i < nodes.size(); ++i) { + nodes[i].velocity = previousVelocity_[i]; + nodes[i].pressure = previousPressure_[i]; + } + } + } + +#ifdef VIENNALS_GPU_BICGSTAB + // Builds the face-major pressure matrix geometry for GPU reuse. + // The logic mirrors the explicit precomputation in solvePressure() so the + // same actualDiagGpu_ / pressCoeffGpu_ arrays can feed both the CPU ILU(0) + // factorization and the GPU SpMV without recomputation. + void buildPressureGpuGeometry() { + const std::size_t n = nodes.size(); + const T eps = std::numeric_limits::epsilon(); + pressCoeffGpu_.assign(2 * D * n, 0.0); + pressNeighId32_.assign(2 * D * n, gpu::kNoNode); + actualDiagGpu_.assign(n, 0.0); + + for (std::size_t id = 0; id < n; ++id) { + if (touchesAmbient_[id]) { + actualDiagGpu_[id] = 1.0; + continue; + } + for (unsigned dir = 0; dir < D; ++dir) { + const unsigned fiNeg = dir * 2u; + const unsigned fiPos = dir * 2u + 1u; + IndexType nbNeg = nodes[id].index; + nbNeg[dir] -= 1; + IndexType nbPos = nodes[id].index; + nbPos[dir] += 1; + const std::size_t jNeg = + inBounds(nbNeg) ? nodeLookupFlat[linearIndex(nbNeg)] : noNode; + const std::size_t jPos = + inBounds(nbPos) ? nodeLookupFlat[linearIndex(nbPos)] : noNode; + + auto effDist = [&](unsigned fi, std::size_t j) -> T { + if (j != noNode) + return gridDelta; + const Boundary bt = faceBCTypes_[fi * n + id]; + return (bt != Boundary::NONE) ? faceBCDists_[fi * n + id] : gridDelta; + }; + + const T dNeg = effDist(fiNeg, jNeg); + const T dPos = effDist(fiPos, jPos); + const T dSum = dNeg + dPos; + if (dSum <= eps) + continue; + + auto processFace = [&](unsigned fi, std::size_t j, T d) { + const T c = T(2) / (d * dSum); + if (j != noNode && !touchesAmbient_[j]) { + pressCoeffGpu_[fi * n + id] = static_cast(c); + pressNeighId32_[fi * n + id] = static_cast(j); + actualDiagGpu_[id] += c; + } else if (j != noNode || + faceBCTypes_[fi * n + id] == Boundary::AMBIENT) { + // j is an ambient-only neighbour (identity-row Dirichlet p=0), OR + // this face crosses the free surface directly (AMBIENT Dirichlet + // p=0 at the sub-grid crossing distance). REACTION faces are + // solid-wall Neumann βˆ‚p/βˆ‚n=0: no contribution. + actualDiagGpu_[id] += c; + } + }; + processFace(fiNeg, jNeg, dNeg); + processFace(fiPos, jPos, dPos); + } + if (actualDiagGpu_[id] <= eps) + actualDiagGpu_[id] = 1.0; + } + } + + // Builds the face-major Stokes velocity matrix geometry for GPU reuse. + // The effective diagonal excludes OOB, AMBIENT, and traction-coupled MASK + // face self-coupling because those cancel exactly with the vBC correction in + // the Stokes matvec. Kinematic MASK faces remain Dirichlet-like and + // contribute to the diagonal. + void buildStokesGpuGeometry() { + const std::size_t n = nodes.size(); + const T eps = std::numeric_limits::epsilon(); + stokesCoeffGpu_.assign(2 * D * n, 0.0); + stokesNeighId32_.assign(2 * D * n, gpu::kNoNode); + stokesDiagGpu_.assign(D * n, 0.0); + + auto addDiag = [&](std::size_t id, T c) { + for (unsigned comp = 0; comp < D; ++comp) + stokesDiagGpu_[comp * n + id] += static_cast(c); + }; + + for (std::size_t id = 0; id < n; ++id) { + for (unsigned dir = 0; dir < D; ++dir) { + const unsigned fiNeg = dir * 2u; + const unsigned fiPos = dir * 2u + 1u; + IndexType nbNeg = nodes[id].index; + nbNeg[dir] -= 1; + IndexType nbPos = nodes[id].index; + nbPos[dir] += 1; + const bool negInBounds = inBounds(nbNeg); + const bool posInBounds = inBounds(nbPos); + const std::size_t jNeg = + negInBounds ? nodeLookupFlat[linearIndex(nbNeg)] : noNode; + const std::size_t jPos = + posInBounds ? nodeLookupFlat[linearIndex(nbPos)] : noNode; + + // Distance matching velocityStencilPoint: gridDelta for OOB/interior, + // faceBCDists_ for boundary-crossing faces. + auto stokesFaceDist = [&](unsigned fi, bool inb, std::size_t j) -> T { + if (!inb || j != noNode) + return gridDelta; + const Boundary bt = faceBCTypes_[fi * n + id]; + return (bt != Boundary::NONE) ? faceBCDists_[fi * n + id] : gridDelta; + }; + + const T dNeg = stokesFaceDist(fiNeg, negInBounds, jNeg); + const T dPos = stokesFaceDist(fiPos, posInBounds, jPos); + const T dSum = dNeg + dPos; + if (dSum <= eps) + continue; + + auto processFace = [&](unsigned fi, bool inb, std::size_t j, T d) { + const T c = T(2) / (d * dSum); + if (inb && j != noNode) { + // Interior neighbor: off-diagonal and diagonal contribution. + stokesCoeffGpu_[fi * n + id] = static_cast(c); + stokesNeighId32_[fi * n + id] = static_cast(j); + addDiag(id, c); + } else if (inb) { + // Boundary-crossing face. + // REACTION / MASK: Dirichlet β†’ adds to diagonal. + // AMBIENT / OOB: excluded (self-coupling cancels with vBC + // correction). + const Boundary bt = faceBCTypes_[fi * n + id]; + if (bt == Boundary::REACTION || bt == Boundary::MASK) + addDiag(id, c); + } + // !inb (OOB): self-coupling, excluded. + }; + processFace(fiNeg, negInBounds, jNeg, dNeg); + processFace(fiPos, posInBounds, jPos, dPos); + } + for (unsigned comp = 0; comp < D; ++comp) + if (stokesDiagGpu_[comp * n + id] <= static_cast(eps)) + stokesDiagGpu_[comp * n + id] = 1.0; + } + } + + // Builds face-major harmonic velocity geometry for GPU reuse. + // The neighbor IDs are identical to Stokes (stokesNeighId32_ is shared). + // + // The CPU harmonicMatvec is Av[i][c] = 2*D*v[i] - stencil_sum(v)[i] + b[i] + // where stencil_sum includes self-coupling (v[nodeId]) for OOB/AMBIENT/NONE + // faces and constants for REACTION/MASK faces. After the b/constant terms + // cancel, the effective matrix diagonal is: + // 2*D - n_OOB - n_AMBIENT - n_NONE = n_interior + n_REACTION + n_MASK + // + // REACTION and MASK faces have no off-diagonal entry (their velocity is a + // constant in b) but they DO count toward the diagonal. Excluding them + // gives a matrix that is not diagonally dominant near the Si/SiO2 boundary, + // which causes Jacobi-preconditioned BiCGSTAB to diverge. + void buildHarmonicGpuGeometry() { + const std::size_t n = nodes.size(); + harmonicCoeffGpu_.assign(2 * D * n, 0.0); + harmonicDiagGpu_.assign(n, 0.0); + for (std::size_t id = 0; id < n; ++id) { + for (unsigned fi = 0; fi < 2 * D; ++fi) { + if (stokesNeighId32_[fi * n + id] != gpu::kNoNode) { + // Interior neighbor: off-diagonal coefficient 1.0 + diagonal 1.0. + harmonicCoeffGpu_[fi * n + id] = 1.0; + harmonicDiagGpu_[id] += 1.0; + } else { + // Non-interior face. REACTION/MASK contribute a Dirichlet constant + // to b but NOT self-coupling, so they add 1.0 to the diagonal just + // like an interior neighbor would (matching the CPU matrix row). + // OOB/AMBIENT/NONE contribute v[nodeId] (self-coupling), which + // cancels with the 2*D diagonal term and must be excluded here. + const Boundary bt = faceBCTypes_[fi * n + id]; + if (bt == Boundary::REACTION || bt == Boundary::MASK) + harmonicDiagGpu_[id] += 1.0; + } + } + if (harmonicDiagGpu_[id] < 1e-10) + harmonicDiagGpu_[id] = 1.0; + } + } + + // Allocates GPU buffers for pressure and Stokes solves and uploads the + // geometry-fixed CSR pattern. Called from buildNodes() after the face BC + // arrays and the two geometry helper arrays are populated. + void setupDeformationGpuBuffers() { + const std::size_t n = nodes.size(); + if (n == 0) { + gpu::freeGpuBuffers(gpuPressBufs_); + gpuPressBufs_ = nullptr; + gpu::freeGpuBuffers(gpuStokesBufs_); + gpuStokesBufs_ = nullptr; + return; + } + const bool tryGpu = (gpuMode_ == GpuMode::Gpu); + const bool useIlu0 = (gpuPreconditioner_ == GpuPreconditioner::ILU0); + + gpu::freeGpuBuffers(gpuPressBufs_); + gpuPressBufs_ = nullptr; + if (tryGpu) { + gpuPressBufs_ = + gpu::allocGpuBuffers(static_cast(n), 2 * D, useIlu0); + if (!gpuPressBufs_) { + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "pressure solver CUDA buffers could not be " + "allocated or the CUDA context could not be " + "initialized." + + gpuErrorDetail()); + } + if (!gpu::gpuUploadNeighborIds(gpuPressBufs_, pressNeighId32_.data(), + 2u * D * n)) { + gpu::freeGpuBuffers(gpuPressBufs_); + gpuPressBufs_ = nullptr; + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "uploading pressure GPU neighbor IDs failed." + + gpuErrorDetail()); + } + if (useIlu0 && !gpu::gpuSetupCSR(gpuPressBufs_, pressNeighId32_.data(), + static_cast(n), 2 * D)) { + gpu::freeGpuBuffers(gpuPressBufs_); + gpuPressBufs_ = nullptr; + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "CUSPARSE setup for the pressure GPU BiCGSTAB " + "solver failed." + + gpuErrorDetail()); + } + } + + gpu::freeGpuBuffers(gpuStokesBufs_); + gpuStokesBufs_ = nullptr; + if (tryGpu) { + gpuStokesBufs_ = + gpu::allocGpuBuffers(static_cast(n), 2 * D, useIlu0); + if (!gpuStokesBufs_) { + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "Stokes solver CUDA buffers could not be " + "allocated or the CUDA context could not be " + "initialized." + + gpuErrorDetail()); + } + if (!gpu::gpuUploadNeighborIds(gpuStokesBufs_, stokesNeighId32_.data(), + 2u * D * n)) { + gpu::freeGpuBuffers(gpuStokesBufs_); + gpuStokesBufs_ = nullptr; + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "uploading Stokes GPU neighbor IDs failed." + + gpuErrorDetail()); + } + if (useIlu0 && !gpu::gpuSetupCSR(gpuStokesBufs_, stokesNeighId32_.data(), + static_cast(n), 2 * D)) { + gpu::freeGpuBuffers(gpuStokesBufs_); + gpuStokesBufs_ = nullptr; + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "CUSPARSE setup for the Stokes GPU BiCGSTAB " + "solver failed." + + gpuErrorDetail()); + } + } + + // Harmonic velocity: same neighbor IDs as Stokes, different coefficients. + gpu::freeGpuBuffers(gpuHarmonicBufs_); + gpuHarmonicBufs_ = nullptr; + if (tryGpu) { + gpuHarmonicBufs_ = + gpu::allocGpuBuffers(static_cast(n), 2 * D, useIlu0); + if (!gpuHarmonicBufs_) { + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "harmonic solver CUDA buffers could not be " + "allocated." + + gpuErrorDetail()); + } + if (!gpu::gpuUploadNeighborIds(gpuHarmonicBufs_, stokesNeighId32_.data(), + 2u * D * n)) { + gpu::freeGpuBuffers(gpuHarmonicBufs_); + gpuHarmonicBufs_ = nullptr; + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "uploading harmonic GPU neighbor IDs failed." + + gpuErrorDetail()); + } + if (useIlu0 && + !gpu::gpuSetupCSR(gpuHarmonicBufs_, stokesNeighId32_.data(), + static_cast(n), 2 * D)) { + gpu::freeGpuBuffers(gpuHarmonicBufs_); + gpuHarmonicBufs_ = nullptr; + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "CUSPARSE setup for the harmonic GPU BiCGSTAB " + "solver failed." + + gpuErrorDetail()); + } + } + + if (tryGpu) + logDeformationBackend("GPU BiCGSTAB", + "pressure/Stokes/harmonic, preconditioner=" + + std::string(useIlu0 ? "ILU0" : "Jacobi")); + } +#endif // VIENNALS_GPU_BICGSTAB + +#ifdef VIENNALS_GPU_BICGSTAB + static std::string gpuErrorDetail() { + const char *detail = gpu::gpuGetLastErrorMessage(); + if (detail && detail[0] != '\0') + return std::string(" Detail: ") + detail; + return {}; + } +#endif + + void logDeformationBackend(const std::string &backend, + const std::string &detail) const { + if (!Logger::hasInfo()) + return; + const std::string msg = "OxidationDeformation: using " + backend + + " for mechanics pressure/Stokes solves (nodes=" + + std::to_string(nodes.size()) + + (detail.empty() ? std::string() : ", " + detail) + + ")."; + if (msg == lastLoggedBackend_) + return; + lastLoggedBackend_ = msg; + Logger::getInstance().addInfo(msg).print(); + } + +public: + bool initialiseGrid() { + return initializeGridFromInterfaces( + reactionInterface, ambientInterface, maskInterface, useRequestedBounds, + requestedMinIndex, requestedMaxIndex, + deformationParameters.maxGridPoints, "OxidationDeformation"); + } + + void buildNodes() { + nodes.clear(); + initNodeLookup(); + + ConstSparseIterator reactionIt(reactionInterface->getDomain()); + ConstSparseIterator ambientIt(ambientInterface->getDomain()); + auto maskIt = makeMaskIterator(); + + IndexType index = minIndex; + while (true) { + const T reactionPhi = valueAt(reactionIt, index); + const T ambientPhi = valueAt(ambientIt, index); + if (isInsideOxide(reactionPhi, ambientPhi) && + !isInsideMask(maskIt, index)) { + const std::size_t id = nodes.size(); + nodeLookupFlat[linearIndex(index)] = id; + nodes.push_back({index}); + } + + if (!increment(index)) + break; + } + + // Precompute per-face boundary intersections into flat face-major arrays. + const std::size_t n = nodes.size(); + faceBCTypes_.assign(2 * D * n, Boundary::NONE); + faceBCDists_.assign(2 * D * n, T(1)); + touchesAmbient_.assign(n, uint8_t(0)); + for (std::size_t id = 0; id < n; ++id) { + const auto &node = nodes[id]; + bool touchesAmbient = false; + bool touchesSolidBoundary = false; + for (unsigned dir = 0; dir < D; ++dir) { + for (int off : {-1, 1}) { + const unsigned fi = dir * 2u + (off == 1 ? 1u : 0u); + IndexType nb = node.index; + nb[dir] += off; + if (!inBounds(nb) || lookupNode(nb) != noNode) + continue; // NONE/1.0 already set + const auto bi = boundaryIntersection(reactionIt, ambientIt, maskIt, + node.index, nb); + faceBCTypes_[fi * n + id] = bi.boundary; + faceBCDists_[fi * n + id] = bi.distance; + if (bi.boundary == Boundary::AMBIENT) + touchesAmbient = true; + else if (bi.boundary == Boundary::MASK || + bi.boundary == Boundary::REACTION) + touchesSolidBoundary = true; + } + } + // Do not collapse mixed mask/reaction/ambient corner nodes to a single + // ambient pressure identity row. Those nodes need their per-face + // boundary conditions; otherwise the mask-edge triple point injects an + // artificial free-surface pressure singularity. + touchesAmbient_[id] = + (touchesAmbient && !touchesSolidBoundary) ? uint8_t(1) : uint8_t(0); + } + +#ifdef VIENNALS_GPU_BICGSTAB + buildPressureGpuGeometry(); + buildStokesGpuGeometry(); + buildHarmonicGpuGeometry(); + setupDeformationGpuBuffers(); +#else + if (gpuMode_ == GpuMode::Gpu) { + VIENNACORE_LOG_ERROR("OxidationDeformation: explicit GPU mode was " + "requested, but ViennaLS was built without " + "VIENNALS_GPU_BICGSTAB."); + } +#endif + } + + // Evaluates the harmonic stencil at one node. + // sum = sum of neighbor/BC contributions (interior neighbors, reaction/mask + // BCs, and self-coupling for OOB/NONE/AMBIENT faces). count is always 2*D + // (every face is counted regardless of type). + template + void computeHarmonicStencilAt(std::size_t nodeId, + const std::vector> &v, + Vec3D &sum) const { + const auto &node = nodes[nodeId]; + sum = {T(0), T(0), T(0)}; + + const auto toT = [](const Vec3D &w) -> Vec3D { + return {static_cast(w[0]), static_cast(w[1]), static_cast(w[2])}; + }; + + for (unsigned direction = 0; direction < D; ++direction) { + for (int offset : {-1, 1}) { + IndexType neighbor = node.index; + neighbor[direction] += offset; + + if (!inBounds(neighbor)) { + detail::vecAddTo(sum, toT(v[nodeId])); // zero-flux: ghost = self + continue; + } + + const std::size_t neighborId = lookupNode(neighbor); + if (neighborId != noNode) { + detail::vecAddTo(sum, toT(v[neighborId])); + continue; + } + + const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u); + const Boundary boundary = faceBCTypes_[fi * nodes.size() + nodeId]; + if (boundary == Boundary::REACTION) { + detail::vecAddTo(sum, reactionBoundaryVelocity(node.index)); + } else if (boundary == Boundary::MASK) { + detail::vecAddTo(sum, + maskVelocityBoundary(node.index, toT(v[nodeId]))); + } else { + detail::vecAddTo(sum, toT(v[nodeId])); // AMBIENT/NONE: zero-flux + } + } + } + } + + // (Av)[i] = (2*D) * v[i] - sum_at_v[i] + b[i] + template + void harmonicMatvec(const std::vector> &v, + const std::vector> &b, + std::vector> &Av) const { + const T diagVal = static_cast(2 * D); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < nodes.size(); ++i) { + Vec3D sum; + computeHarmonicStencilAt(i, v, sum); + for (unsigned c = 0; c < D; ++c) + Av[i][c] = static_cast(diagVal * v[i][c] - sum[c] + b[i][c]); + } + } + + void solveVelocity() { + iterations = 0; + residual = 0.; + if (nodes.empty()) + return; + + using SolverT = T; + + const std::size_t n = nodes.size(); + const T diagVal = static_cast(2 * D); // constant for all nodes + + // b[i] = BC constants (reaction + mask velocities), computed at v = zeros. + // OOB/NONE/AMBIENT faces contribute v[i] = 0 at zeros, so only Dirichlet + // BCs survive β€” correctly isolating the RHS constant vector. + std::vector> b(n); + { + const std::vector> zeros( + n, Vec3D{SolverT(0), SolverT(0), SolverT(0)}); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) + computeHarmonicStencilAt(i, zeros, b[i]); + } + + // Warm-start from previous substep's velocity field. + std::vector> x(n); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + const T value = nodes[i].velocity[c]; + x[i][c] = static_cast(std::isfinite(value) ? value : T(0)); + } + +#ifdef VIENNALS_GPU_BICGSTAB + if (gpu::gpuIsValid(gpuHarmonicBufs_)) { + const std::size_t nf = 2u * D * n; + if (harmonicDiagGpu_.size() != n || harmonicCoeffGpu_.size() != nf) { + VIENNACORE_LOG_ERROR("OxidationDeformation: harmonic GPU geometry has " + "the wrong size for the current node set."); + } + + Timer<> tUpload, tSolve; + std::vector> xSolved(n); + unsigned maxGpuIterations = 0; + double maxGpuResidual = 0.0; + + for (unsigned c = 0; c < D; ++c) { + std::vector bGpu(n), xGpu(n); + for (std::size_t i = 0; i < n; ++i) { + bGpu[i] = static_cast(b[i][c]); + xGpu[i] = static_cast(x[i][c]); + } + + tUpload.start(); + const bool gpuUploadOk = + (c == 0) ? gpu::gpuUploadSolverArrays( + gpuHarmonicBufs_, harmonicDiagGpu_.data(), + bGpu.data(), harmonicCoeffGpu_.data(), + static_cast(n), harmonicCoeffGpu_.size()) + : gpu::gpuUploadRhs(gpuHarmonicBufs_, bGpu.data(), + static_cast(n)); + tUpload.finish(); + if (!gpuUploadOk) { + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, " + "but uploading harmonic solver arrays failed." + + gpuErrorDetail()); + } + + unsigned gpuIterations = 0; + double gpuResidual = 0.0; + tSolve.start(); + const bool gpuConverged = gpu::gpuSolveBiCGSTAB( + gpuHarmonicBufs_, xGpu.data(), + static_cast(std::numeric_limits::epsilon()), + deformationParameters.harmonicIterations, + static_cast(deformationParameters.tolerance), gpuIterations, + gpuResidual); + tSolve.finish(); + + if (!gpuConverged || !std::isfinite(gpuResidual)) { + VIENNACORE_LOG_ERROR( + "OxidationDeformation: harmonic GPU BiCGSTAB failed or produced " + "a non-finite residual for component " + + std::to_string(c) + " (iters=" + std::to_string(gpuIterations) + + ", residual=" + std::to_string(gpuResidual) + ")."); + } + + maxGpuIterations = std::max(maxGpuIterations, gpuIterations); + maxGpuResidual = std::max(maxGpuResidual, gpuResidual); + for (std::size_t i = 0; i < n; ++i) + xSolved[i][c] = static_cast(xGpu[i]); + } + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + nodes[i].velocity[c] = static_cast(xSolved[i][c]); + iterations = maxGpuIterations; + residual = maxGpuResidual; + + if (Logger::hasTiming()) { + Logger::getInstance() + .addTiming("harmonic n=" + std::to_string(n) + + " iters=" + std::to_string(iterations) + " res=" + + std::to_string(residual) + " [GPU] GPU BiCGSTAB", + tSolve) + .print(); + } + if (Logger::hasDebug()) { + Logger::getInstance() + .addTiming("harmonic n=" + std::to_string(n) + " [GPU] GPU upload", + tUpload) + .print(); + } + return; + } +#endif + + // r = b - A*x + const Vec3D zero3{SolverT(0), SolverT(0), SolverT(0)}; + std::vector> Ax(n); + harmonicMatvec(x, b, Ax); + std::vector> r(n), r_hat(n); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + r[i][c] = static_cast(b[i][c] - Ax[i][c]); + r_hat[i][c] = r[i][c]; + } + + // BiCGSTAB with diagonal preconditioner (diag = 2*D, constant). + std::vector> pv(n, zero3), sv(n, zero3), y(n), z(n), s(n), + t(n); + T rho = T(1), alpha = T(1), omega = T(1); + + auto vecDot = [&](const std::vector> &a, + const std::vector> &bv) { + T sum = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + sum += static_cast(a[i][c]) * static_cast(bv[i][c]); + return sum; + }; + + auto vecMaxAbs = [&](const std::vector> &vin) { + T m = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + m = std::max(m, std::abs(static_cast(vin[i][c]))); + return m; + }; + + const T b_norm = [&] { + T m = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + m = std::max(m, std::abs(b[i][c])); + return (m < T(1e-100)) ? T(1) : m; + }(); + + for (; iterations < deformationParameters.harmonicIterations; + ++iterations) { + const T rho_new = vecDot(r_hat, r); + if (!std::isfinite(rho_new) || std::abs(rho_new) < T(1e-100)) + break; + if (!std::isfinite(rho) || !std::isfinite(alpha) || + !std::isfinite(omega) || std::abs(omega) < T(1e-100)) + break; + + const T beta = (rho_new / rho) * (alpha / omega); + if (!std::isfinite(beta)) + break; + rho = rho_new; + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + pv[i][c] = static_cast(r[i][c] + + beta * (pv[i][c] - omega * sv[i][c])); + + // y = M^{-1} p = p / (2*D) + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + y[i][c] = static_cast(static_cast(pv[i][c]) / diagVal); + + harmonicMatvec(y, b, sv); + + const T r_hat_v = vecDot(r_hat, sv); + if (!std::isfinite(r_hat_v) || std::abs(r_hat_v) < T(1e-100)) + break; + + alpha = rho_new / r_hat_v; + if (!std::isfinite(alpha)) + break; + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + s[i][c] = static_cast(r[i][c] - alpha * sv[i][c]); + + residual = vecMaxAbs(s); + if (!std::isfinite(residual)) + break; + if (residual < deformationParameters.tolerance * b_norm) { + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + x[i][c] = static_cast(x[i][c] + alpha * y[i][c]); + ++iterations; + break; + } + + // z = M^{-1} s + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + z[i][c] = static_cast(static_cast(s[i][c]) / diagVal); + + harmonicMatvec(z, b, t); + + const T t_s = vecDot(t, s); + const T t_t = vecDot(t, t); + if (!std::isfinite(t_s) || !std::isfinite(t_t)) + break; + omega = (t_t > T(1e-100)) ? t_s / t_t : T(0); + if (!std::isfinite(omega)) + break; + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + x[i][c] = + static_cast(x[i][c] + alpha * y[i][c] + omega * z[i][c]); + r[i][c] = static_cast(s[i][c] - omega * t[i][c]); + } + + residual = vecMaxAbs(r); + if (!std::isfinite(residual)) + break; + if (residual < deformationParameters.tolerance * b_norm) { + ++iterations; + break; + } + } + + bool finiteSolution = true; + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + if (!std::isfinite(static_cast(x[i][c]))) + finiteSolution = false; + + if (finiteSolution) { + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + nodes[i].velocity[c] = static_cast(x[i][c]); + } else { + residual = std::numeric_limits::infinity(); + } + if (residual > deformationParameters.tolerance * b_norm) + VIENNACORE_LOG_WARNING( + "solveVelocity (harmonic): BiCGSTAB did not converge after " + + std::to_string(iterations) + "/" + + std::to_string(deformationParameters.harmonicIterations) + + " iterations (residual=" + std::to_string(residual / b_norm) + + ", tolerance=" + std::to_string(deformationParameters.tolerance) + + ")"); + } + + // Returns component-wise diagonal entries of the Stokes operator A_v. + // Geometry-fixed within a mechanics solve; computed once and reused by the + // SIMPLE velocity-correction step: v_c^{k+1}=v_c* - grad_c(dp)/(eta*a_ic). + // + // With traction-coupled MASK contact, ghost=v_node+const for every component, + // so the MASK face self-coupling cancels and the face coefficient is removed. + std::vector> computeVelocityDiagonals() const { + const std::size_t n = nodes.size(); + std::vector> diagV(n, Vec3D{T(0), T(0), T(0)}); + if (n == 0) + return diagV; + const std::vector> zeros(n, Vec3D{T(0), T(0), T(0)}); + std::vector> tmp(n); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) { + T diag{}; + computeVelocityStencilAt(i, zeros, diag, tmp[i]); + for (unsigned comp = 0; comp < D; ++comp) + diagV[i][comp] = diag; + } + + return diagV; + } + + void solveMechanics() { + T mechanicsResidual = 0.; + + // SIMPLE (Semi-Implicit Method for Pressure-Linked Equations) coupling. + // The Gauss-Seidel pβ†’vβ†’p loop has spectral radius > 1 on thin geometries, + // causing divergence that worsens with more iterations. SIMPLE adds a + // velocity-correction step after the pressure update that provably + // eliminates the oscillation mode: + // + // 1. Momentum predictor: A_v * v* = vBC - (βˆ‡p^k - βˆ‡Β·Οƒ'(v^k)) / Ξ· + // 2. Pressure update: A_p * p^{k+1} = pBC + K Β· div(v*) + // 3. Velocity correction: v^{k+1} = v* - βˆ‡Ξ΄p / (Ξ· Β· a_i) + // where Ξ΄p = p^{k+1} - p^k, a_i = diag(A_v)[i] + // + // Step 3 ensures the corrected velocity is consistent with the new + // pressure without re-solving the full momentum equation. Unlike the + // Aitken clamp (which can only damp, not stabilise, ρ > 1 iterations), + // this correction is unconditionally convergent for steady Stokes. + + const std::vector> diagV = + computeVelocityDiagonals(); // geometry-fixed within this call + + for (unsigned iteration = 0; + iteration < deformationParameters.mechanicsIterations; ++iteration) { + const auto previousVelocity = collectVelocities(); // v^k + const auto previousPressure = collectPressures(); // p^k + + computeDiagnostics(); + computeStressTensors(); + + // Step 1: momentum predictor uses current p^k (in nodes[i].pressure). + Timer<> tStokes, tPressure; + tStokes.start(); + solveStokesVelocity(); // produces v* in nodes[i].velocity + tStokes.finish(); + if (!std::isfinite(lastStokesResidual_)) { + mechanicsResidual = std::numeric_limits::infinity(); + break; + } + + // Step 2: pressure solve uses divergence of v*. + tPressure.start(); + solvePressure(); // produces p^{k+1} in nodes[i].pressure + tPressure.finish(); + if (!std::isfinite(lastPressureResidual_)) { + mechanicsResidual = std::numeric_limits::infinity(); + break; + } + + // Step 3: SIMPLE velocity correction: v^{k+1} = v* - βˆ‡Ξ΄p / (Ξ· Β· a_i). + applySimpleVelocityCorrection(previousPressure, diagV); + + mechanicsResidual = std::max(maxVelocityChange(previousVelocity), + maxPressureChange(previousPressure)); + if (!std::isfinite(mechanicsResidual)) { + mechanicsResidual = std::numeric_limits::infinity(); + break; + } + + if (Logger::hasDebug()) + Logger::getInstance() + .addTiming( + " mechanics[" + std::to_string(iteration) + + "] stokes iters=" + std::to_string(lastStokesIters_) + + "/" + + std::to_string(deformationParameters.stokesIterations) + + " res=" + std::to_string(lastStokesResidual_), + tStokes) + .addTiming( + " mechanics[" + std::to_string(iteration) + + "] pressure iters=" + std::to_string(lastPressureIters_) + + "/" + + std::to_string(deformationParameters.pressureIterations) + + " res=" + std::to_string(lastPressureResidual_) + + " coupling=" + std::to_string(mechanicsResidual), + tPressure) + .print(); + + if (mechanicsResidual < deformationParameters.mechanicsTolerance) + break; + } + + computeDiagnostics(); + computeStressTensors(); + residual = mechanicsResidual; + if (residual > deformationParameters.mechanicsTolerance) + VIENNACORE_LOG_WARNING( + "solveMechanics: did not converge after " + + std::to_string(deformationParameters.mechanicsIterations) + + " iterations (residual=" + std::to_string(residual) + ", tolerance=" + + std::to_string(deformationParameters.mechanicsTolerance) + ")"); + } + + // SIMPLE velocity correction: v^{k+1} = v* - βˆ‡(p^{k+1} - p^k) / (Ξ· Β· a_i) + // + // Ξ΄p gradient uses homogeneous Neumann at all boundary faces (Ξ΄p ghost = 0). + // The boundary pressure correction is re-enforced by the next pressure solve, + // so this approximation only affects the current-iteration correction, not + // the converged solution. + void applySimpleVelocityCorrection(const std::vector &pressureOld, + const std::vector> &diagV) { + if (nodes.empty()) + return; + if (deformationParameters.viscosity <= std::numeric_limits::epsilon()) + return; + + const std::size_t n = nodes.size(); + const T invEta = T(1) / deformationParameters.viscosity; + +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) { + for (unsigned dir = 0; dir < D; ++dir) { + const T ai = diagV[i][dir]; + if (ai <= std::numeric_limits::epsilon()) + continue; + + // Negative-offset face (fi = dir*2). + T dpMinus, dMinus; + { + const unsigned fi = dir * 2u; + IndexType nb = nodes[i].index; + nb[dir] -= 1; + if (inBounds(nb)) { + const std::size_t j = nodeLookupFlat[linearIndex(nb)]; + if (j != noNode) { + dpMinus = nodes[j].pressure - pressureOld[j]; + dMinus = gridDelta; + } else { + dpMinus = T(0); + dMinus = faceBCDists_[fi * n + i]; + } + } else { + dpMinus = T(0); + dMinus = gridDelta; + } + } + + // Positive-offset face (fi = dir*2+1). + T dpPlus, dPlus; + { + const unsigned fi = dir * 2u + 1u; + IndexType nb = nodes[i].index; + nb[dir] += 1; + if (inBounds(nb)) { + const std::size_t j = nodeLookupFlat[linearIndex(nb)]; + if (j != noNode) { + dpPlus = nodes[j].pressure - pressureOld[j]; + dPlus = gridDelta; + } else { + dpPlus = T(0); + dPlus = faceBCDists_[fi * n + i]; + } + } else { + dpPlus = T(0); + dPlus = gridDelta; + } + } + + const T dpCenter = nodes[i].pressure - pressureOld[i]; + const T gradDP = + firstDerivative(dpMinus, dpCenter, dpPlus, dMinus, dPlus); + const T correction = + gradDP * invEta / ai * deformationParameters.relaxation; + if (std::isfinite(correction) && std::isfinite(nodes[i].velocity[dir])) + nodes[i].velocity[dir] -= correction; + } + } + } + + // Fills diag = centerCoefficient and rhs = pressureSum for one node. + // Dirichlet (ambient) nodes are encoded as identity rows: diag=1, + // rhs=ambientBP. + template + void computePressureStencilAt(std::size_t nodeId, + const std::vector &p, + const std::vector &ambientBP, T &diag, + T &rhs) const { + if (touchesAmbient_[nodeId]) { + diag = T(1); + rhs = ambientBP[nodeId]; + return; + } + diag = T(0); + rhs = T(0); + for (unsigned direction = 0; direction < D; ++direction) { + const auto plus = + pressureStencilPoint(p, ambientBP, nodeId, direction, 1); + const auto minus = + pressureStencilPoint(p, ambientBP, nodeId, direction, -1); + const T dSum = plus.distance + minus.distance; + const T plusCoeff = T(2) / (plus.distance * dSum); + const T minusCoeff = T(2) / (minus.distance * dSum); + rhs += plusCoeff * plus.value + minusCoeff * minus.value; + diag += plusCoeff + minusCoeff; + } + } + + // (Av)[i] = precomputedDiag[i]*v[i] - rhs_at_v[i] + pBC[i] + template + void + pressureMatvec(const std::vector &v, const std::vector &ambientBP, + const std::vector &precomputedDiag, + const std::vector &pBC, std::vector &Av) const { +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < nodes.size(); ++i) { + T diag, rhs; + computePressureStencilAt(i, v, ambientBP, diag, rhs); + Av[i] = static_cast(precomputedDiag[i] * v[i] - rhs + pBC[i]); + } + } + + void solvePressure() { + if (nodes.empty()) + return; + + using SolverT = T; + + const std::size_t n = nodes.size(); + const T eps = std::numeric_limits::epsilon(); + + std::vector divergence(n), ambientBP(n); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) { + divergence[i] = divergenceAt(nodes[i].index); + ambientBP[i] = freeSurfacePressureBoundary(nodes[i].index); + } + + auto warnBadPressureAssembly = [](const std::string &stage, + std::size_t nodeId, const IndexType &idx, + T value) { + VIENNACORE_LOG_WARNING( + "solvePressure: non-finite/overflow " + stage + + " at node=" + std::to_string(nodeId) + " index=(" + + std::to_string(idx[0]) + "," + std::to_string(idx[1]) + + (D == 3 ? "," + std::to_string(idx[2]) : std::string()) + + ") value=" + std::to_string(value)); + }; + + const T solverMax = static_cast(std::numeric_limits::max()); + for (std::size_t i = 0; i < n; ++i) { + if (!std::isfinite(divergence[i]) || + std::abs(divergence[i]) > solverMax) { + warnBadPressureAssembly("divergence", i, nodes[i].index, divergence[i]); + break; + } + if (!std::isfinite(ambientBP[i]) || std::abs(ambientBP[i]) > solverMax) { + warnBadPressureAssembly("ambient pressure boundary", i, nodes[i].index, + ambientBP[i]); + break; + } + } + + // Geometry-fixed diagonal and BC constants (kept in T for full precision). + std::vector diag(n), pBC(n); + { + const std::vector zeros(n, SolverT(0)); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) + computePressureStencilAt(i, zeros, ambientBP, diag[i], pBC[i]); + } + + for (std::size_t i = 0; i < n; ++i) { + if (!std::isfinite(diag[i]) || std::abs(diag[i]) > solverMax) { + warnBadPressureAssembly("pressure diagonal", i, nodes[i].index, + diag[i]); + break; + } + if (!std::isfinite(pBC[i]) || std::abs(pBC[i]) > solverMax) { + warnBadPressureAssembly("pressure boundary rhs", i, nodes[i].index, + pBC[i]); + break; + } + } + + std::vector b(n); + T b_norm = T(0); + for (std::size_t i = 0; i < n; ++i) { + b[i] = pBC[i] + deformationParameters.bulkModulus * divergence[i]; + b_norm = std::max(b_norm, std::abs(b[i])); + } + for (std::size_t i = 0; i < n; ++i) { + if (!std::isfinite(b[i]) || std::abs(b[i]) > solverMax) { + warnBadPressureAssembly("pressure rhs", i, nodes[i].index, b[i]); + break; + } + } + if (b_norm < T(1e-100)) + b_norm = T(1); + + std::vector x(n); + for (std::size_t i = 0; i < n; ++i) { + T guess = touchesAmbient_[i] ? ambientBP[i] : nodes[i].pressure; + if (!std::isfinite(guess)) + guess = deformationParameters.ambientPressure; + x[i] = static_cast(guess); + } + +#ifdef VIENNALS_GPU_BICGSTAB + if (gpu::gpuIsValid(gpuPressBufs_)) { + const std::size_t nf = 2u * D * n; + if (actualDiagGpu_.size() != n || pressCoeffGpu_.size() != nf) { + VIENNACORE_LOG_ERROR("OxidationDeformation: pressure GPU geometry has " + "the wrong size for the current node set."); + } + + Timer<> tUpload, tSolve; + std::vector bGpu(n), xGpu(n); + for (std::size_t i = 0; i < n; ++i) { + bGpu[i] = static_cast(b[i]); + xGpu[i] = static_cast(x[i]); + } + + tUpload.start(); + const bool gpuUploadOk = gpu::gpuUploadSolverArrays( + gpuPressBufs_, actualDiagGpu_.data(), bGpu.data(), + pressCoeffGpu_.data(), static_cast(n), + pressCoeffGpu_.size()); + tUpload.finish(); + if (!gpuUploadOk) { + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, but " + "uploading pressure solver arrays or factorizing " + "ILU failed." + + gpuErrorDetail()); + } + + unsigned gpuIterations = 0; + double gpuResidual = 0.0; + tSolve.start(); + const bool gpuConverged = gpu::gpuSolveBiCGSTAB( + gpuPressBufs_, xGpu.data(), static_cast(eps), + deformationParameters.pressureIterations, + static_cast(deformationParameters.pressureTolerance), + gpuIterations, gpuResidual); + tSolve.finish(); + + // gpuResidual is the GPU true residual ||b - A*x||_inf recomputed at + // convergence (not the recursive BiCGSTAB residual), so no separate CPU + // stencil evaluation is needed. + if (!gpuConverged || !std::isfinite(gpuResidual)) { + VIENNACORE_LOG_ERROR( + "OxidationDeformation: pressure GPU BiCGSTAB failed or produced " + "a non-finite residual (iters=" + + std::to_string(gpuIterations) + + ", residual=" + std::to_string(gpuResidual) + ")."); + } + + { + const T beta = deformationParameters.pressureRelaxation; + const T oneMinB = T(1) - beta; + for (std::size_t i = 0; i < n; ++i) + nodes[i].pressure = + oneMinB * nodes[i].pressure + beta * static_cast(xGpu[i]); + } + lastPressureIters_ = gpuIterations; + lastPressureResidual_ = gpuResidual / b_norm; + + if (Logger::hasDebug()) { + const std::string tag = + "pressure n=" + std::to_string(n) + + " iters=" + std::to_string(lastPressureIters_) + + " res=" + std::to_string(lastPressureResidual_) + " [GPU]"; + Logger::getInstance() + .addTiming(tag + " GPU upload", tUpload) + .addTiming(tag + " GPU BiCGSTAB", tSolve) + .print(); + } + return; + } +#endif + + // Precompute off-diagonal structure and the CORRECT matrix diagonal for + // SSOR. + // + // Key insight: diag[i] from computePressureStencilAt includes self-coupling + // contributions from REACTION/MASK/OOB faces (those return v[nodeId] + // itself). The ACTUAL matrix diagonal A[i,i] = sum of off-diagonal + // (interior-neighbor) coefficients only. Using the wrong diagonal in the + // SSOR sweeps makes the preconditioner invalid near boundaries. + // + // Also: NONE-type non-interior faces (OOB or no crossing) use gridDelta in + // pressureStencilPoint, NOT faceBCDists_ (which defaults to T(1)). + // + // Face-major layout: fi = dir*2 + (offset==+1 ? 1 : 0) + // Even fi (offset=-1): lower-index neighbor β†’ forward sweep + // Odd fi (offset=+1): higher-index neighbor β†’ backward sweep + std::vector pressCoeff(2 * D * n, T(0)); + std::vector pressNeighId(2 * D * n, noNode); + std::vector actualDiag(n, T(0)); // A[i,i] = sum of interior coefficients + + for (std::size_t id = 0; id < n; ++id) { + if (touchesAmbient_[id]) { + actualDiag[id] = T(1); + continue; + } // identity row + for (unsigned dir = 0; dir < D; ++dir) { + const unsigned fiNeg = dir * 2u; + const unsigned fiPos = dir * 2u + 1u; + IndexType nbNeg = nodes[id].index; + nbNeg[dir] -= 1; + IndexType nbPos = nodes[id].index; + nbPos[dir] += 1; + const std::size_t jNeg = + inBounds(nbNeg) ? nodeLookupFlat[linearIndex(nbNeg)] : noNode; + const std::size_t jPos = + inBounds(nbPos) ? nodeLookupFlat[linearIndex(nbPos)] : noNode; + + // Effective distance matching pressureStencilPoint: + // interior neighbour β†’ gridDelta + // AMBIENT/REACTION/MASK crossing β†’ faceBCDists_ (actual sub-grid + // distance) NONE (OOB or no crossing) β†’ gridDelta + // (pressureStencilPoint fallthrough) + auto effectiveDist = [&](unsigned fi, std::size_t j) -> T { + if (j != noNode) + return gridDelta; + const Boundary bt = faceBCTypes_[fi * n + id]; + if (bt != Boundary::NONE) + return faceBCDists_[fi * n + id]; + return gridDelta; + }; + + const T dNeg = effectiveDist(fiNeg, jNeg); + const T dPos = effectiveDist(fiPos, jPos); + const T dSum = dNeg + dPos; + if (dSum <= eps) + continue; + + if (jNeg != noNode && !touchesAmbient_[jNeg]) { + const T c = T(2) / (dNeg * dSum); + pressCoeff[fiNeg * n + id] = c; + pressNeighId[fiNeg * n + id] = jNeg; + actualDiag[id] += c; // A[i,i] += interior off-diagonal coefficient + } else if (jNeg != noNode || + faceBCTypes_[fiNeg * n + id] == Boundary::AMBIENT) { + // j is an ambient-only neighbour (identity-row Dirichlet p=0), OR + // this face directly crosses the free surface (AMBIENT Dirichlet p=0 + // at the sub-grid crossing distance). RHS contribution is cΒ·0=0. + // REACTION faces are solid-wall Neumann βˆ‚p/βˆ‚n=0: no contribution. + actualDiag[id] += T(2) / (dNeg * dSum); + } + if (jPos != noNode && !touchesAmbient_[jPos]) { + const T c = T(2) / (dPos * dSum); + pressCoeff[fiPos * n + id] = c; + pressNeighId[fiPos * n + id] = jPos; + actualDiag[id] += c; + } else if (jPos != noNode || + faceBCTypes_[fiPos * n + id] == Boundary::AMBIENT) { + actualDiag[id] += T(2) / (dPos * dSum); + } + } + // Guard against fully-isolated nodes (surrounded by boundaries on every + // face) + if (actualDiag[id] <= eps) + actualDiag[id] = T(1); + } + + // ILU(0) preconditioner for the (non-symmetric) pressure matrix. + // + // The sub-grid interface distances make A[i,j] β‰  A[j,i] in general, so + // SSOR is not guaranteed to converge. ILU(0) handles non-symmetric + // matrices robustly. + // + // Factorisation A β‰ˆ L * U (zero fill-in, natural node ordering): + // L – unit lower triangular: L[i,j] = A[i,j] / U[j,j] for j < i + // U – upper triangular: U[i,j] = A[i,j] for j > i + // U[i,i] = A[i,i] - Ξ£_{ki} pressCoeff[fi_U*n+i] * z[j]) / ilu_diag[i] + std::vector ilu_diag(n); + for (std::size_t id = 0; id < n; ++id) { + if (touchesAmbient_[id]) { + ilu_diag[id] = T(1); + continue; + } + ilu_diag[id] = actualDiag[id]; + for (unsigned dir = 0; dir < D; ++dir) { + const unsigned fi_L = dir * 2u; // lower face (offset=-1) + const unsigned fi_U = + fi_L + 1u; // upper face (offset=+1, j's face toward i) + const std::size_t j = pressNeighId[fi_L * n + id]; + if (j == noNode || ilu_diag[j] <= eps) + continue; + // L[id,j] = A[id,j] / U[j,j] = (-pressCoeff_L) / ilu_diag[j] + // A[j,id] = -pressCoeff[fi_U * n + j] (j's upper-face coefficient + // toward id) ilu_diag[id] -= L[id,j] * A[j,id] + // = (-pressCoeff_L / ilu_diag[j]) * (-pressCoeff_fi_U[j]) + // = pressCoeff_L * pressCoeff_fi_U[j] / ilu_diag[j] + // (positive drop) + ilu_diag[id] -= + pressCoeff[fi_L * n + id] * pressCoeff[fi_U * n + j] / ilu_diag[j]; + } + if (ilu_diag[id] <= eps) + ilu_diag[id] = actualDiag[id]; // guard non-positive pivot + } + + auto applyIlu = [&](const std::vector &in, + std::vector &out) { + std::vector y(n); + // Forward solve: L * y = in (L is unit lower triangular) + for (std::size_t i = 0; i < n; ++i) { + T val = static_cast(in[i]); + for (unsigned dir = 0; dir < D; ++dir) { + const unsigned fi_L = dir * 2u; + const std::size_t j = pressNeighId[fi_L * n + i]; + if (j != noNode) + // L[i,j] = -pressCoeff[fi_L*n+i] / ilu_diag[j], subtract + // A[i,j]*y[j]: y[i] -= L[i,j] * y[j] = -(-pressCoeff/ilu_diag[j]) * + // y[j] = +(coeff/ilu) * y[j] + val += (pressCoeff[fi_L * n + i] / ilu_diag[j]) * y[j]; + } + y[i] = val; // no diagonal divide (unit lower triangular) + } + // Backward solve: U * z = y + for (std::size_t i = n; i-- > 0;) { + T val = y[i]; + for (unsigned dir = 0; dir < D; ++dir) { + const unsigned fi_U = dir * 2u + 1u; + const std::size_t j = pressNeighId[fi_U * n + i]; + if (j != noNode) + // U[i,j] = -pressCoeff[fi_U*n+i], subtract U[i,j]*z[j]: + // val -= U[i,j] * z[j] = -(-pressCoeff) * z[j] = +(pressCoeff) * + // z[j] + val += pressCoeff[fi_U * n + i] * static_cast(out[j]); + } + out[i] = static_cast(val / ilu_diag[i]); + } + }; + + std::vector Ax(n); + pressureMatvec(x, ambientBP, diag, pBC, Ax); + for (std::size_t i = 0; i < n; ++i) { + if (!std::isfinite(static_cast(Ax[i])) || + std::abs(static_cast(Ax[i])) > solverMax) { + warnBadPressureAssembly("initial pressure matvec", i, nodes[i].index, + static_cast(Ax[i])); + break; + } + } + std::vector r(n), r_hat(n), p(n, SolverT(0)), v(n, SolverT(0)), + y(n), z(n), s(n), t(n); + for (std::size_t i = 0; i < n; ++i) { + r[i] = static_cast(b[i] - Ax[i]); + r_hat[i] = r[i]; + } + + T rho = T(1), alpha = T(1), omega = T(1); + T pressureResidual = T(0); + unsigned pressureIter = 0; + bool pressureBreakdown = false; + for (std::size_t i = 0; i < n; ++i) { + const T ri = static_cast(r[i]); + if (!std::isfinite(ri)) { + pressureBreakdown = true; + break; + } + pressureResidual = std::max(pressureResidual, std::abs(ri)); + } + + for (; !pressureBreakdown && + pressureIter < deformationParameters.pressureIterations; + ++pressureIter) { + T rho_new = T(0); + for (std::size_t i = 0; i < n; ++i) + rho_new += static_cast(r_hat[i]) * static_cast(r[i]); + + if (!std::isfinite(rho_new)) { + pressureBreakdown = true; + break; + } + if (std::abs(rho_new) < T(1e-100)) + break; + if (!std::isfinite(rho) || !std::isfinite(alpha) || + !std::isfinite(omega) || std::abs(omega) < T(1e-100)) { + pressureBreakdown = true; + break; + } + + const T beta = (rho_new / rho) * (alpha / omega); + if (!std::isfinite(beta)) { + pressureBreakdown = true; + break; + } + rho = rho_new; + + for (std::size_t i = 0; i < n; ++i) + p[i] = static_cast(r[i] + beta * (p[i] - omega * v[i])); + + applyIlu(p, y); + + pressureMatvec(y, ambientBP, diag, pBC, v); + + T r_hat_v = T(0); + for (std::size_t i = 0; i < n; ++i) + r_hat_v += static_cast(r_hat[i]) * static_cast(v[i]); + if (!std::isfinite(r_hat_v)) { + pressureBreakdown = true; + break; + } + if (std::abs(r_hat_v) < T(1e-100)) + break; + + alpha = rho_new / r_hat_v; + if (!std::isfinite(alpha)) { + pressureBreakdown = true; + break; + } + + for (std::size_t i = 0; i < n; ++i) + s[i] = static_cast(r[i] - alpha * v[i]); + + pressureResidual = T(0); + for (std::size_t i = 0; i < n; ++i) + pressureResidual = + std::max(pressureResidual, std::abs(static_cast(s[i]))); + if (!std::isfinite(pressureResidual)) { + pressureBreakdown = true; + break; + } + if (pressureResidual < deformationParameters.pressureTolerance * b_norm) { + for (std::size_t i = 0; i < n; ++i) + x[i] = static_cast(x[i] + alpha * y[i]); + break; + } + + applyIlu(s, z); + + pressureMatvec(z, ambientBP, diag, pBC, t); + + T t_s = T(0), t_t = T(0); + for (std::size_t i = 0; i < n; ++i) { + t_s += static_cast(t[i]) * static_cast(s[i]); + t_t += static_cast(t[i]) * static_cast(t[i]); + } + if (!std::isfinite(t_s) || !std::isfinite(t_t)) { + pressureBreakdown = true; + break; + } + omega = (t_t > T(1e-100)) ? t_s / t_t : T(0); + if (!std::isfinite(omega)) { + pressureBreakdown = true; + break; + } + + for (std::size_t i = 0; i < n; ++i) { + x[i] = static_cast(x[i] + alpha * y[i] + omega * z[i]); + r[i] = static_cast(s[i] - omega * t[i]); + } + + pressureResidual = T(0); + for (std::size_t i = 0; i < n; ++i) + pressureResidual = + std::max(pressureResidual, std::abs(static_cast(r[i]))); + if (!std::isfinite(pressureResidual)) { + pressureBreakdown = true; + break; + } + if (pressureResidual < deformationParameters.pressureTolerance * b_norm) + break; + } + + if (pressureBreakdown) + pressureResidual = std::numeric_limits::infinity(); + + bool finiteSolution = !pressureBreakdown; + for (std::size_t i = 0; i < n; ++i) + if (!std::isfinite(static_cast(x[i]))) + finiteSolution = false; + + lastPressureIters_ = pressureIter; + lastPressureResidual_ = pressureResidual / b_norm; + if (finiteSolution) { + const T beta = deformationParameters.pressureRelaxation; + const T oneMinB = T(1) - beta; + for (std::size_t i = 0; i < n; ++i) + nodes[i].pressure = + oneMinB * nodes[i].pressure + beta * static_cast(x[i]); + } else { + lastPressureResidual_ = std::numeric_limits::infinity(); + } + if (lastPressureResidual_ > deformationParameters.pressureTolerance) + VIENNACORE_LOG_WARNING( + "solvePressure: BiCGSTAB did not converge after " + + std::to_string(lastPressureIters_) + "/" + + std::to_string(deformationParameters.pressureIterations) + + " iterations (residual=" + std::to_string(lastPressureResidual_) + + ", tolerance=" + + std::to_string(deformationParameters.pressureTolerance) + ")"); + } + + // Fills scalar diag = centerCoefficient and Vec3D rhs = velocitySum for one + // node. + template + void computeVelocityStencilAt(std::size_t nodeId, + const std::vector> &v, T &diag, + Vec3D &rhs) const { + diag = T(0); + rhs = {T(0), T(0), T(0)}; + for (unsigned direction = 0; direction < D; ++direction) { + const auto plus = velocityStencilPoint(v, nodeId, direction, 1); + const auto minus = velocityStencilPoint(v, nodeId, direction, -1); + const T dSum = plus.distance + minus.distance; + const T plusCoeff = T(2) / (plus.distance * dSum); + const T minusCoeff = T(2) / (minus.distance * dSum); + detail::vecAddTo(rhs, detail::vecScaled(plus.value, plusCoeff)); + detail::vecAddTo(rhs, detail::vecScaled(minus.value, minusCoeff)); + diag += plusCoeff + minusCoeff; + } + } + + void solveStokesVelocity() { + if (deformationParameters.viscosity <= std::numeric_limits::epsilon()) + return; + if (nodes.empty()) + return; + + using SolverT = T; + + const std::size_t n = nodes.size(); + const T eps = std::numeric_limits::epsilon(); + + // Geometry-fixed diagonal, BC constants, and forcing (all in T). + std::vector diag(n); + std::vector> vBC(n), forcing(n); + { + const std::vector> zeros( + n, Vec3D{SolverT(0), SolverT(0), SolverT(0)}); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) { + computeVelocityStencilAt(i, zeros, diag[i], vBC[i]); + forcing[i] = momentumForcing(nodes[i].index); + } + } + const auto precondDiag = computeVelocityDiagonals(); + + std::vector> b(n); + T b_norm = T(0); + for (std::size_t i = 0; i < n; ++i) { + for (unsigned c = 0; c < D; ++c) { + b[i][c] = vBC[i][c] - forcing[i][c] / deformationParameters.viscosity; + b_norm = std::max(b_norm, std::abs(b[i][c])); + } + } + if (b_norm < T(1e-100)) + b_norm = T(1); + + // Initial guess from current node velocities (warm-start), converted to + // SolverT. + std::vector> x(n); + { + const auto vel = collectVelocities(); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + const T value = vel[i][c]; + x[i][c] = static_cast(std::isfinite(value) ? value : T(0)); + } + } + +#ifdef VIENNALS_GPU_BICGSTAB + if (gpu::gpuIsValid(gpuStokesBufs_)) { + const std::size_t nf = 2u * D * n; + if (stokesDiagGpu_.size() != D * n || stokesCoeffGpu_.size() != nf) { + VIENNACORE_LOG_ERROR("OxidationDeformation: Stokes GPU geometry has " + "the wrong size for the current node set."); + } + + Timer<> tUpload, tSolve; + std::vector> xSolved(n); + unsigned maxGpuIterations = 0; + double maxGpuResidual = 0.0; + + for (unsigned c = 0; c < D; ++c) { + std::vector bGpu(n), xGpu(n); + for (std::size_t i = 0; i < n; ++i) { + bGpu[i] = static_cast(b[i][c]); + xGpu[i] = static_cast(x[i][c]); + } + + tUpload.start(); + const bool gpuUploadOk = gpu::gpuUploadSolverArrays( + gpuStokesBufs_, stokesDiagGpu_.data() + c * n, bGpu.data(), + stokesCoeffGpu_.data(), static_cast(n), + stokesCoeffGpu_.size()); + tUpload.finish(); + if (!gpuUploadOk) { + VIENNACORE_LOG_ERROR("OxidationDeformation: GPU mode was selected, " + "but uploading Stokes solver arrays failed." + + gpuErrorDetail()); + } + + unsigned gpuIterations = 0; + double gpuResidual = 0.0; + tSolve.start(); + const bool gpuConverged = gpu::gpuSolveBiCGSTAB( + gpuStokesBufs_, xGpu.data(), static_cast(eps), + deformationParameters.stokesIterations, + static_cast(deformationParameters.stokesTolerance), + gpuIterations, gpuResidual); + tSolve.finish(); + + if (!gpuConverged || !std::isfinite(gpuResidual)) { + VIENNACORE_LOG_ERROR( + "OxidationDeformation: Stokes GPU BiCGSTAB failed or produced " + "a non-finite residual for component " + + std::to_string(c) + " (iters=" + std::to_string(gpuIterations) + + ", residual=" + std::to_string(gpuResidual) + ")."); + } + + maxGpuIterations = std::max(maxGpuIterations, gpuIterations); + maxGpuResidual = std::max(maxGpuResidual, gpuResidual); + for (std::size_t i = 0; i < n; ++i) + xSolved[i][c] = static_cast(xGpu[i]); + } + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + nodes[i].velocity[c] = static_cast(xSolved[i][c]); + + lastStokesIters_ = maxGpuIterations; + lastStokesResidual_ = maxGpuResidual / b_norm; + + if (Logger::hasDebug()) { + const std::string tag = "stokes n=" + std::to_string(n) + + " iters=" + std::to_string(lastStokesIters_) + + " res=" + std::to_string(lastStokesResidual_) + + " [GPU]"; + Logger::getInstance() + .addTiming(tag + " GPU upload", tUpload) + .addTiming(tag + " GPU BiCGSTAB", tSolve) + .print(); + } + return; + } +#endif + + // Stokes SpMV: (Av)[i] = diag[i]*vin[i] - rhs_at_vin[i] + vBC[i], stored as + // SolverT. + auto stokesMatvec = [&](const std::vector> &vin, + std::vector> &Av) { +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) { + T d; + Vec3D rhs; + computeVelocityStencilAt(i, vin, d, rhs); + for (unsigned c = 0; c < D; ++c) + Av[i][c] = + static_cast(diag[i] * vin[i][c] - rhs[c] + vBC[i][c]); + } + }; + + // Dot product accumulated in T for numerical stability. + auto vecDot = [&](const std::vector> &a, + const std::vector> &bv) { + T sum = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + const T av = static_cast(a[i][c]); + const T bvVal = static_cast(bv[i][c]); + if (!std::isfinite(av) || !std::isfinite(bvVal)) + return std::numeric_limits::quiet_NaN(); + sum += av * bvVal; + } + return sum; + }; + + auto vecMaxAbs = [&](const std::vector> &vin) { + T m = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + const T value = static_cast(vin[i][c]); + if (!std::isfinite(value)) + return std::numeric_limits::infinity(); + m = std::max(m, std::abs(value)); + } + return m; + }; + + // r = b - A*x + const Vec3D zero3{SolverT(0), SolverT(0), SolverT(0)}; + std::vector> Ax(n), r(n), r_hat(n); + std::vector> pv(n, zero3), sv(n, zero3), y(n), z(n), s(n), + t(n); + stokesMatvec(x, Ax); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + r[i][c] = static_cast(b[i][c] - Ax[i][c]); + r_hat[i][c] = r[i][c]; + } + + T rho = T(1), alpha = T(1), omega = T(1); + T velocityResidual = T(0); + unsigned stokesIter = 0; + bool stokesBreakdown = false; + velocityResidual = vecMaxAbs(r); + + for (; stokesIter < deformationParameters.stokesIterations; ++stokesIter) { + const T rho_new = vecDot(r_hat, r); + if (!std::isfinite(rho_new)) { + stokesBreakdown = true; + break; + } + if (std::abs(rho_new) < T(1e-100)) + break; + if (!std::isfinite(rho) || !std::isfinite(alpha) || + !std::isfinite(omega) || std::abs(omega) < T(1e-100)) { + stokesBreakdown = true; + break; + } + + const T beta = (rho_new / rho) * (alpha / omega); + if (!std::isfinite(beta)) { + stokesBreakdown = true; + break; + } + rho = rho_new; + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + pv[i][c] = static_cast(r[i][c] + + beta * (pv[i][c] - omega * sv[i][c])); + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + const T pvc = pv[i][c]; + const T pcDiag = precondDiag[i][c]; + y[i][c] = static_cast((pcDiag > eps) ? pvc / pcDiag : pvc); + } + + stokesMatvec(y, sv); + + const T r_hat_v = vecDot(r_hat, sv); + if (!std::isfinite(r_hat_v)) { + stokesBreakdown = true; + break; + } + if (std::abs(r_hat_v) < T(1e-100)) + break; + + alpha = rho_new / r_hat_v; + if (!std::isfinite(alpha)) { + stokesBreakdown = true; + break; + } + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + s[i][c] = static_cast(r[i][c] - alpha * sv[i][c]); + + velocityResidual = vecMaxAbs(s); + if (!std::isfinite(velocityResidual)) { + stokesBreakdown = true; + break; + } + if (velocityResidual < deformationParameters.stokesTolerance * b_norm) { + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + x[i][c] = static_cast(x[i][c] + alpha * y[i][c]); + break; + } + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + const T sc = s[i][c]; + const T pcDiag = precondDiag[i][c]; + z[i][c] = static_cast((pcDiag > eps) ? sc / pcDiag : sc); + } + + stokesMatvec(z, t); + + const T t_s = vecDot(t, s); + const T t_t = vecDot(t, t); + if (!std::isfinite(t_s) || !std::isfinite(t_t)) { + stokesBreakdown = true; + break; + } + omega = (t_t > T(1e-100)) ? t_s / t_t : T(0); + if (!std::isfinite(omega)) { + stokesBreakdown = true; + break; + } + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + x[i][c] = + static_cast(x[i][c] + alpha * y[i][c] + omega * z[i][c]); + r[i][c] = static_cast(s[i][c] - omega * t[i][c]); + } + + velocityResidual = vecMaxAbs(r); + if (!std::isfinite(velocityResidual)) { + stokesBreakdown = true; + break; + } + if (velocityResidual < deformationParameters.stokesTolerance * b_norm) + break; + } + + if (stokesBreakdown) + velocityResidual = std::numeric_limits::infinity(); + + bool finiteSolution = !stokesBreakdown; + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + if (!std::isfinite(static_cast(x[i][c]))) + finiteSolution = false; + + if (finiteSolution) { + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + nodes[i].velocity[c] = static_cast(x[i][c]); + } + + lastStokesIters_ = stokesIter; + lastStokesResidual_ = finiteSolution ? velocityResidual / b_norm + : std::numeric_limits::infinity(); + if (lastStokesResidual_ > deformationParameters.stokesTolerance) + VIENNACORE_LOG_WARNING( + "solveStokesVelocity: BiCGSTAB did not converge after " + + std::to_string(lastStokesIters_) + "/" + + std::to_string(deformationParameters.stokesIterations) + + " iterations (residual=" + std::to_string(lastStokesResidual_) + + ", tolerance=" + + std::to_string(deformationParameters.stokesTolerance) + ")"); + } + + std::vector> collectVelocities() const { + std::vector> velocities; + velocities.reserve(nodes.size()); + for (const auto &node : nodes) + velocities.push_back(node.velocity); + return velocities; + } + + std::vector collectPressures() const { + std::vector pressures; + pressures.reserve(nodes.size()); + for (const auto &node : nodes) + pressures.push_back(node.pressure); + return pressures; + } + + template + StencilPoint + pressureStencilPoint(const std::vector &pressure, + const std::vector &ambientBoundaryPressure, + std::size_t nodeId, unsigned direction, + int offset) const { + const auto &node = nodes[nodeId]; + IndexType neighbor = node.index; + neighbor[direction] += offset; + + if (!inBounds(neighbor)) + return {static_cast(pressure[nodeId]), gridDelta}; + + const std::size_t neighborId = nodeLookupFlat[linearIndex(neighbor)]; + if (neighborId != noNode) { + if (touchesAmbient_[neighborId]) + return {ambientBoundaryPressure[neighborId], gridDelta}; + return {static_cast(pressure[neighborId]), gridDelta}; + } + + const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u); + const std::size_t nn = nodes.size(); + const Boundary faceType = faceBCTypes_[fi * nn + nodeId]; + const T faceDist = faceBCDists_[fi * nn + nodeId]; + if (faceType == Boundary::AMBIENT) + return {ambientBoundaryPressure[nodeId], faceDist}; + // Reaction interface: Neumann βˆ‚p/βˆ‚n=0 (solid-wall BC for pressure). + // The ghost node takes the same value as the interior node, giving zero + // contribution to the Laplacian stencil. The pressure is anchored only by + // the AMBIENT Dirichlet (p=0 at the free surface), which is always present + // for any connected oxide region. + if (faceType == Boundary::REACTION) + return {static_cast(pressure[nodeId]), faceDist}; + if (faceType == Boundary::MASK) + return {maskPressureBoundary(node.index, direction, offset, + static_cast(pressure[nodeId])), + faceDist}; + + return {static_cast(pressure[nodeId]), gridDelta}; + } + + template + StencilPoint> + velocityStencilPoint(const std::vector> &velocity, + std::size_t nodeId, unsigned direction, + int offset) const { + const auto &node = nodes[nodeId]; + IndexType neighbor = node.index; + neighbor[direction] += offset; + + const auto toT = [](const Vec3D &v) -> Vec3D { + return {static_cast(v[0]), static_cast(v[1]), static_cast(v[2])}; + }; + + if (!inBounds(neighbor)) + return {toT(velocity[nodeId]), gridDelta}; + + const std::size_t neighborId = nodeLookupFlat[linearIndex(neighbor)]; + if (neighborId != noNode) + return {toT(velocity[neighborId]), gridDelta}; + + const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u); + const std::size_t nn = nodes.size(); + const Boundary faceType = faceBCTypes_[fi * nn + nodeId]; + const T faceDist = faceBCDists_[fi * nn + nodeId]; + if (faceType == Boundary::REACTION) + return {reactionBoundaryVelocity(node.index), faceDist}; + if (faceType == Boundary::AMBIENT) + return {freeSurfaceVelocityBoundary(node.index, direction, offset, + faceDist, toT(velocity[nodeId])), + faceDist}; + if (faceType == Boundary::MASK) + return {maskVelocityBoundary(node.index, toT(velocity[nodeId])), + faceDist}; + + return {toT(velocity[nodeId]), gridDelta}; + } + + StencilPoint currentPressureStencilPoint(std::size_t nodeId, + unsigned direction, + int offset) const { + const auto &node = nodes[nodeId]; + IndexType neighbor = node.index; + neighbor[direction] += offset; + + if (!inBounds(neighbor)) + return {node.pressure, gridDelta}; + + const std::size_t neighborId = nodeLookupFlat[linearIndex(neighbor)]; + if (neighborId != noNode) + return {nodes[neighborId].pressure, gridDelta}; + + const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u); + const std::size_t nn = nodes.size(); + const Boundary faceType = faceBCTypes_[fi * nn + nodeId]; + const T faceDist = faceBCDists_[fi * nn + nodeId]; + if (faceType == Boundary::AMBIENT) + return {freeSurfacePressureBoundary(node.index), faceDist}; + if (faceType == Boundary::REACTION) + return {node.pressure, faceDist}; // Neumann βˆ‚p/βˆ‚n=0 + if (faceType == Boundary::MASK) + return { + maskPressureBoundary(node.index, direction, offset, node.pressure), + faceDist}; + + return {node.pressure, gridDelta}; + } + + StencilPoint> currentVelocityStencilPoint(std::size_t nodeId, + unsigned direction, + int offset) const { + const auto &node = nodes[nodeId]; + IndexType neighbor = node.index; + neighbor[direction] += offset; + + if (!inBounds(neighbor)) + return {node.velocity, gridDelta}; + + const std::size_t neighborId = nodeLookupFlat[linearIndex(neighbor)]; + if (neighborId != noNode) + return {nodes[neighborId].velocity, gridDelta}; + + const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u); + const std::size_t nn = nodes.size(); + const Boundary faceType = faceBCTypes_[fi * nn + nodeId]; + const T faceDist = faceBCDists_[fi * nn + nodeId]; + if (faceType == Boundary::REACTION) + return {reactionBoundaryVelocity(node.index), faceDist}; + if (faceType == Boundary::AMBIENT) + return {freeSurfaceVelocityBoundary(node.index, direction, offset, + faceDist, node.velocity), + faceDist}; + if (faceType == Boundary::MASK) + return {maskVelocityBoundary(node.index, node.velocity), faceDist}; + + return {node.velocity, gridDelta}; + } + + T maxVelocityChange(const std::vector> &previous) const { + T maxChange = 0.; + T maxVelocity = 0.; + const auto count = std::min(previous.size(), nodes.size()); + for (std::size_t i = 0; i < count; ++i) { + for (unsigned j = 0; j < D; ++j) { + maxChange = std::max(maxChange, + std::abs(nodes[i].velocity[j] - previous[i][j])); + maxVelocity = std::max(maxVelocity, std::abs(nodes[i].velocity[j])); + } + } + + if (maxVelocity <= std::numeric_limits::epsilon()) + return maxChange; + return maxChange / maxVelocity; + } + + T maxPressureChange(const std::vector &previous) const { + T maxChange = 0.; + T maxPressure = 0.; + const auto count = std::min(previous.size(), nodes.size()); + for (std::size_t i = 0; i < count; ++i) { + maxChange = + std::max(maxChange, std::abs(nodes[i].pressure - previous[i])); + maxPressure = std::max(maxPressure, std::abs(nodes[i].pressure)); + } + + if (maxPressure <= std::numeric_limits::epsilon()) + return maxChange; + return maxChange / maxPressure; + } + + std::array + currentBoundaryDeviatoricStress(const IndexType &index) const { + const auto strainRate = strainRateTensorAt(index); + const auto deviatoricRate = + deviatoricTensor(strainRate, divergenceAt(index)); + const auto previousStress = previousDeviatoricStress(index); + const T relaxationTime = effectiveStressRelaxationTime(); + const T decay = + (relaxationTime <= std::numeric_limits::epsilon()) + ? T(0) + : std::exp(-deformationParameters.stressTimeStep / relaxationTime); + + std::array deviatoricStress{}; + for (unsigned i = 0; i < 9; ++i) { + const T viscousStress = + T(2) * deformationParameters.viscosity * deviatoricRate[i]; + deviatoricStress[i] = + decay * previousStress[i] + (T(1) - decay) * viscousStress; + } + + return deviatoricStress; + } + + T freeSurfacePressureBoundary(const IndexType &index) const { + const auto normal = interfaceNormal(index, Boundary::AMBIENT); + const auto deviatoricStress = currentBoundaryDeviatoricStress(index); + + return deformationParameters.ambientPressure + + normalStress(deviatoricStress, normal); + } + + T maskPressureBoundary(const IndexType & /*index*/, unsigned /*direction*/, + int /*offset*/, T fallbackPressure) const { + return fallbackPressure; + } + + Vec3D freeSurfaceVelocityBoundary(const IndexType &index, + unsigned direction, int offset, + T distance, + const Vec3D &interiorVelocity) const { + Vec3D boundaryVelocity = interiorVelocity; + const auto normal = interfaceNormal(index, Boundary::AMBIENT); + const auto deviatoricStress = deviatoricStressAt(index); + const T pressure = pressureAt(index); + + Vec3D deviatoricTraction{0., 0., 0.}; + for (unsigned component = 0; component < D; ++component) { + for (unsigned j = 0; j < D; ++j) + deviatoricTraction[component] += + deviatoricStress[tensorIndex(component, j)] * normal[j]; + } + + for (unsigned component = 0; component < D; ++component) { + const T normalTraction = + pressure * normal[component] - deviatoricTraction[component]; + const T faceDerivative = normalTraction * normal[direction] / + std::max(deformationParameters.viscosity, + std::numeric_limits::epsilon()); + boundaryVelocity[component] += + static_cast(offset) * distance * faceDerivative; + } + + return boundaryVelocity; + } + + Vec3D maskVelocityBoundary(const IndexType &index, + const Vec3D &interiorVelocity) const { + if (maskVelocityField != nullptr) { + Vec3D coordinate{0., 0., 0.}; + for (unsigned i = 0; i < D; ++i) + coordinate[i] = index[i] * gridDelta; + return maskVelocityField->getVectorVelocity( + coordinate, deformationParameters.material, {0., 0., 0.}, 0); + } + return {0., 0., 0.}; + } + + void computeAvgExpansionSpeed() { + avgExpansionSpeed_ = 0.; + if (nodes.empty()) + return; + + ConstSparseIterator reactionIt(reactionInterface->getDomain()); + ConstSparseIterator ambientIt(ambientInterface->getDomain()); + auto maskIt = makeMaskIterator(); + std::size_t count = 0; + + for (const auto &node : nodes) { + bool touchesReactionBoundary = false; + for (unsigned direction = 0; direction < D; ++direction) { + for (int offset : {-1, 1}) { + IndexType neighbor = node.index; + neighbor[direction] += offset; + if (!inBounds(neighbor)) + continue; + + if (lookupNode(neighbor) != noNode) + continue; + + if (classifyBoundary(reactionIt, ambientIt, maskIt, node.index, + neighbor) == Boundary::REACTION) { + touchesReactionBoundary = true; + break; + } + } + if (touchesReactionBoundary) + break; + } + + if (touchesReactionBoundary) { + Vec3D coordinate{0., 0., 0.}; + for (unsigned i = 0; i < D; ++i) + coordinate[i] = node.index[i] * gridDelta; + avgExpansionSpeed_ += + (oxidationParameters.expansionCoefficient - T(1)) * + std::abs(diffusionField->getScalarVelocity(coordinate, 0, + {0., 0., 0.}, 0)); + ++count; + } + } + + if (count > 0) + avgExpansionSpeed_ /= static_cast(count); + } + + Vec3D reactionBoundaryVelocity(const IndexType &index) const { + Vec3D coordinate{0., 0., 0.}; + for (unsigned i = 0; i < D; ++i) + coordinate[i] = index[i] * gridDelta; + const T expansionVelocity = localExpansionSpeed(coordinate); + return detail::vecScaled(reactionNormal(index), + reactionSign * expansionVelocity); + } + + Vec3D unresolvedAmbientVelocity(const Vec3D &coordinate) const { + if (diffusionField == nullptr || ambientInterface == nullptr) + return {0., 0., 0.}; + + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + + ConstSparseIterator ambientIt(ambientInterface->getDomain()); + const auto normal = levelSetNormal(ambientIt, index); + return detail::vecScaled(normal, localExpansionSpeed(coordinate)); + } + + Vec3D estimateMaxUnresolvedAmbientVelocity() const { + Vec3D maxVelocity{0., 0., 0.}; + if (ambientInterface == nullptr || diffusionField == nullptr) + return maxVelocity; + + ConstSparseIterator ambientIt(ambientInterface->getDomain()); + for (; !ambientIt.isFinished(); ++ambientIt) { + if (!ambientIt.isDefined()) + continue; + + Vec3D coordinate{0., 0., 0.}; + const auto &index = ambientIt.getStartIndices(); + for (unsigned d = 0; d < D; ++d) + coordinate[d] = index[d] * gridDelta; + + const auto velocity = unresolvedAmbientVelocity(coordinate); + for (unsigned d = 0; d < D; ++d) + maxVelocity[d] = std::max(maxVelocity[d], std::abs(velocity[d])); + } + return maxVelocity; + } + + T divergenceAt(const IndexType &index) const { + T divergence = 0.; + for (unsigned i = 0; i < D; ++i) { + divergence += velocityDerivative(index, i, i); + } + return divergence; + } + + Vec3D pressureGradient(const IndexType &index) const { + Vec3D gradient{0., 0., 0.}; + for (unsigned i = 0; i < D; ++i) + gradient[i] = pressureDerivative(index, i); + return gradient; + } + + Vec3D momentumForcing(const IndexType &index) const { + Vec3D forcing = pressureGradient(index); + const auto stressDivergence = deviatoricStressDivergence(index); + for (unsigned i = 0; i < D; ++i) + forcing[i] -= stressDivergence[i]; + return forcing; + } + + Vec3D deviatoricStressDivergence(const IndexType &index) const { + Vec3D divergence{0., 0., 0.}; + for (unsigned component = 0; component < D; ++component) { + for (unsigned direction = 0; direction < D; ++direction) { + IndexType pos = index; + IndexType neg = index; + pos[direction] += 1; + neg[direction] -= 1; + const auto posStress = deviatoricStressAt(pos); + const auto negStress = deviatoricStressAt(neg); + divergence[component] += + (posStress[tensorIndex(component, direction)] - + negStress[tensorIndex(component, direction)]) / + (T(2) * gridDelta); + } + } + return divergence; + } + + std::array deviatoricStressAt(const IndexType &index) const { + if (!inBounds(index)) + return {}; + + const std::size_t nodeId = lookupNode(index); + if (nodeId == noNode) + return {}; + + std::array deviatoric = nodes[nodeId].stressTensor; + for (unsigned i = 0; i < 3; ++i) + deviatoric[tensorIndex(i, i)] += nodes[nodeId].pressure; + return deviatoric; + } + + T pressureAt(const IndexType &index) const { + if (!inBounds(index)) + return deformationParameters.ambientPressure; + + const std::size_t nodeId = lookupNode(index); + if (nodeId == noNode) + return deformationParameters.ambientPressure; + return nodes[nodeId].pressure; + } + + T localExpansionSpeed(const Vec3D &coordinate) const { + return (oxidationParameters.expansionCoefficient - T(1)) * + std::abs(diffusionField->getScalarVelocity(coordinate, 0, + {0., 0., 0.}, 0)); + } + + Vec3D reactionNormal(const IndexType &index) const { + ConstSparseIterator reactionIt(reactionInterface->getDomain()); + return levelSetNormal(reactionIt, index); + } + + Vec3D interfaceNormal(const IndexType &index, Boundary boundary) const { + if (boundary == Boundary::AMBIENT) { + ConstSparseIterator ambientIt(ambientInterface->getDomain()); + return levelSetNormal(ambientIt, index); + } + if (boundary == Boundary::MASK && maskInterface != nullptr) { + ConstSparseIterator maskIt(maskInterface->getDomain()); + return levelSetNormal(maskIt, index); + } + + ConstSparseIterator reactionIt(reactionInterface->getDomain()); + return levelSetNormal(reactionIt, index); + } + + Vec3D levelSetNormal(ConstSparseIterator &levelSetIt, + const IndexType &index) const { + Vec3D normal{0., 0., 0.}; + T norm = 0.; + + for (unsigned i = 0; i < D; ++i) { + IndexType pos = index; + IndexType neg = index; + pos[i] += 1; + neg[i] -= 1; + if (!inBounds(pos)) + pos = index; + if (!inBounds(neg)) + neg = index; + normal[i] = detail::clampLevelSetPhi(valueAt(levelSetIt, pos)) - + detail::clampLevelSetPhi(valueAt(levelSetIt, neg)); + norm += normal[i] * normal[i]; + } + + if (norm <= std::numeric_limits::epsilon()) { + normal = Vec3D{0., 0., 0.}; + normal[D - 1] = 1.; + return normal; + } + + norm = std::sqrt(norm); + for (unsigned i = 0; i < D; ++i) + normal[i] /= norm; + return normal; + } + + void computeDiagnostics() { +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < nodes.size(); ++i) + nodes[i].strainTrace = divergenceAt(nodes[i].index); + } + + void computeStressTensors() { + const T relaxationTime = effectiveStressRelaxationTime(); + const T decay = + (relaxationTime <= std::numeric_limits::epsilon()) + ? T(0) + : std::exp(-deformationParameters.stressTimeStep / relaxationTime); + + // Per-node computation is independent; collect history keys into a vector + // to avoid concurrent map writes, then build the map sequentially below. + std::vector>> historyEntries( + nodes.size()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < nodes.size(); ++i) { + auto &node = nodes[i]; + node.strainRateTensor = strainRateTensorAt(node.index); + const auto deviatoricRate = + deviatoricTensor(node.strainRateTensor, node.strainTrace); + const auto previousStress = previousDeviatoricStress(node.index); + + std::array deviatoricStress{}; + for (unsigned j = 0; j < 9; ++j) { + const T viscousStress = + T(2) * deformationParameters.viscosity * deviatoricRate[j]; + deviatoricStress[j] = + decay * previousStress[j] + (T(1) - decay) * viscousStress; + } + + node.stressTensor = deviatoricStress; + for (unsigned j = 0; j < 3; ++j) + node.stressTensor[tensorIndex(j, j)] -= node.pressure; + + node.vonMisesStress = vonMisesFromDeviatoric(deviatoricStress); + historyEntries[i] = {node.index, deviatoricStress}; + } + + std::unordered_map, detail::IndexTypeHasher> + nextHistory; + nextHistory.reserve(nodes.size()); + for (const auto &entry : historyEntries) + nextHistory[entry.first] = entry.second; + deviatoricStressHistory.swap(nextHistory); + } + + std::array strainRateTensorAt(const IndexType &index) const { + std::array tensor{}; + for (unsigned i = 0; i < D; ++i) { + for (unsigned j = 0; j < D; ++j) { + tensor[tensorIndex(i, j)] = T(0.5) * (velocityDerivative(index, i, j) + + velocityDerivative(index, j, i)); + } + } + return tensor; + } + + T velocityDerivative(const IndexType &index, unsigned component, + unsigned direction) const { + const std::size_t nodeId = lookupNode(index); + if (nodeId == noNode) + return 0.; + + const auto plus = currentVelocityStencilPoint(nodeId, direction, 1); + const auto minus = currentVelocityStencilPoint(nodeId, direction, -1); + return firstDerivative( + minus.value[component], nodes[nodeId].velocity[component], + plus.value[component], minus.distance, plus.distance); + } + + T pressureDerivative(const IndexType &index, unsigned direction) const { + const std::size_t nodeId = lookupNode(index); + if (nodeId == noNode) + return 0.; + + const auto plus = currentPressureStencilPoint(nodeId, direction, 1); + const auto minus = currentPressureStencilPoint(nodeId, direction, -1); + return firstDerivative(minus.value, nodes[nodeId].pressure, plus.value, + minus.distance, plus.distance); + } + + std::array deviatoricTensor(const std::array &tensor, + T trace) const { + std::array result = tensor; + const T mean = trace / T(3); + for (unsigned i = 0; i < 3; ++i) + result[tensorIndex(i, i)] -= mean; + return result; + } + + std::array previousDeviatoricStress(const IndexType &index) const { + const auto found = deviatoricStressHistory.find(index); + if (found == deviatoricStressHistory.end()) + return {}; + return found->second; + } + + T effectiveStressRelaxationTime() const { + if (deformationParameters.stressRelaxationTime > T(0)) + return deformationParameters.stressRelaxationTime; + if (deformationParameters.shearModulus > std::numeric_limits::epsilon()) + return deformationParameters.viscosity / + deformationParameters.shearModulus; + return T(0); + } + + T vonMisesFromDeviatoric(const std::array &deviatoricStress) const { + T doubleContraction = 0.; + for (unsigned i = 0; i < 3; ++i) { + for (unsigned j = 0; j < 3; ++j) { + const T value = T(0.5) * (deviatoricStress[tensorIndex(i, j)] + + deviatoricStress[tensorIndex(j, i)]); + doubleContraction += value * value; + } + } + return std::sqrt(T(1.5) * doubleContraction); + } + + T normalStress(const std::array &tensor, const Vec3D &normal) const { + T result = 0.; + for (unsigned i = 0; i < 3; ++i) { + for (unsigned j = 0; j < 3; ++j) + result += normal[i] * tensor[tensorIndex(i, j)] * normal[j]; + } + return result; + } + + Boundary classifyBoundary(ConstSparseIterator &reactionIt, + ConstSparseIterator &ambientIt, + ConstSparseIterator &maskIt, + const IndexType &inside, + const IndexType &outside) const { + return boundaryIntersection(reactionIt, ambientIt, maskIt, inside, outside) + .boundary; + } + + BoundaryIntersection boundaryIntersection(ConstSparseIterator &reactionIt, + ConstSparseIterator &ambientIt, + ConstSparseIterator &maskIt, + const IndexType &inside, + const IndexType &outside) const { + const T reactionInside = valueAt(reactionIt, inside); + const T reactionOutside = valueAt(reactionIt, outside); + const T ambientInside = valueAt(ambientIt, inside); + const T ambientOutside = valueAt(ambientIt, outside); + const T maskInside = valueAtMask(maskIt, inside); + const T maskOutside = valueAtMask(maskIt, outside); + + const bool reactionCrosses = crosses(reactionInside, reactionOutside); + const bool ambientCrosses = crosses(ambientInside, ambientOutside); + const bool maskCrosses = + maskInterface != nullptr && crosses(maskInside, maskOutside); + + if (!reactionCrosses && !ambientCrosses && !maskCrosses) + return {Boundary::NONE, gridDelta}; + if (reactionCrosses && !ambientCrosses && !maskCrosses) + return {Boundary::REACTION, + crossingDistance(reactionInside, reactionOutside)}; + if (!reactionCrosses && ambientCrosses && !maskCrosses) + return ambientCrossingInsideMask( + maskInside, maskOutside, + crossingDistance(ambientInside, ambientOutside)); + if (!reactionCrosses && !ambientCrosses && maskCrosses) + return {Boundary::MASK, crossingDistance(maskInside, maskOutside)}; + + const T reactionDistance = + reactionCrosses ? crossingDistance(reactionInside, reactionOutside) + : std::numeric_limits::max(); + const T ambientDistance = + ambientCrosses ? crossingDistance(ambientInside, ambientOutside) + : std::numeric_limits::max(); + const T maskDistance = maskCrosses + ? crossingDistance(maskInside, maskOutside) + : std::numeric_limits::max(); + if (reactionDistance <= ambientDistance && reactionDistance <= maskDistance) + return {Boundary::REACTION, reactionDistance}; + if (ambientDistance != std::numeric_limits::max()) { + const auto maskedAmbient = + ambientCrossingInsideMask(maskInside, maskOutside, ambientDistance); + if (maskedAmbient.boundary == Boundary::MASK) + return maskedAmbient; + } + if (maskDistance <= ambientDistance) + return {Boundary::MASK, maskDistance}; + return {Boundary::AMBIENT, ambientDistance}; + } + + bool touchesBoundary(ConstSparseIterator &reactionIt, + ConstSparseIterator &ambientIt, + ConstSparseIterator &maskIt, const IndexType &index, + Boundary requestedBoundary) const { + for (unsigned direction = 0; direction < D; ++direction) { + for (int offset : {-1, 1}) { + IndexType neighbor = index; + neighbor[direction] += offset; + if (!inBounds(neighbor)) + continue; + + if (lookupNode(neighbor) != noNode) + continue; + + if (classifyBoundary(reactionIt, ambientIt, maskIt, index, neighbor) == + requestedBoundary) + return true; + } + } + return false; + } + + bool isInsideOxide(T reactionPhi, T ambientPhi) const { + // GeometricAdvect can leave a tiny positive residual (~4*epsilon) when the + // interface lands exactly on a grid point at non-zero coordinates, because + // k*gridDelta is not exactly representable in floating point. Allow a + // tolerance of 1e-9 grid units so that grid points on the surface (phiβ‰ˆ0) + // are correctly classified as inside the oxide. + constexpr T eps = T(1e-9); + return reactionSign * reactionPhi >= -eps && + ambientSign * ambientPhi >= -eps; + } + + ConstSparseIterator makeMaskIterator() const { + if (maskInterface == nullptr) + return ConstSparseIterator(reactionInterface->getDomain()); + return ConstSparseIterator(maskInterface->getDomain()); + } + + bool isInsideMask(ConstSparseIterator &maskIt, const IndexType &index) const { + if (maskInterface == nullptr) + return false; + return maskSign * valueAt(maskIt, index) >= 0.; + } + + T valueAtMask(ConstSparseIterator &maskIt, const IndexType &index) const { + if (maskInterface == nullptr) + return std::numeric_limits::max(); + return valueAt(maskIt, index); + } + + BoundaryIntersection ambientCrossingInsideMask(T maskInside, T maskOutside, + T distance) const { + if (isMaskAtCrossing(maskInside, maskOutside, distance)) + return {Boundary::MASK, distance}; + // Outer node is wholly inside the mask body: the oxide/gas surface has + // drifted into the nitride. Apply mask Dirichlet BC (not traction-free) + // so the deformation solver does not advance the surface further in. + // Mirrors the equivalent check in lsOxidationDiffusion::classifyBoundary. + if (maskInterface != nullptr && + static_cast(maskSign) * maskOutside >= T(0)) + return {Boundary::MASK, distance}; + return {Boundary::AMBIENT, distance}; + } + + bool isMaskAtCrossing(T maskInside, T maskOutside, T distance) const { + if (maskInterface == nullptr) + return false; + const T fraction = std::clamp(distance / gridDelta, T(0), T(1)); + const T insidePhi = detail::clampLevelSetPhi(maskInside); + const T outsidePhi = detail::clampLevelSetPhi(maskOutside); + const T maskPhi = insidePhi + fraction * (outsidePhi - insidePhi); + return static_cast(maskSign) * maskPhi >= T(0); + } + + T crossingDistance(T insidePhi, T outsidePhi) const { + return detail::levelSetCrossingDistance( + insidePhi, outsidePhi, + deformationParameters.minMechanicsBoundaryDistance, gridDelta); + } + + static T firstDerivative(T minusValue, T centerValue, T plusValue, + T minusDistance, T plusDistance) { + const T denominator = + minusDistance * plusDistance * (minusDistance + plusDistance); + if (denominator <= std::numeric_limits::epsilon()) + return 0.; + + return (-plusDistance * plusDistance * minusValue + + (plusDistance * plusDistance - minusDistance * minusDistance) * + centerValue + + minusDistance * minusDistance * plusValue) / + denominator; + } + + static constexpr unsigned tensorIndex(unsigned row, unsigned column) { + return 3 * row + column; + } +}; + +} // namespace viennals diff --git a/include/viennals/lsOxidationDiffusion.hpp b/include/viennals/lsOxidationDiffusion.hpp new file mode 100644 index 00000000..c1cbc1b8 --- /dev/null +++ b/include/viennals/lsOxidationDiffusion.hpp @@ -0,0 +1,1564 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#ifdef VIENNALS_GPU_BICGSTAB +#include +#endif + +namespace viennals { + +/// Selects the BiCGSTAB back-end for the diffusion solve. +/// GPU failures are reported and not silently fallen back to CPU. +enum class GpuMode { + Cpu, ///< Always use CPU (default) + Gpu ///< Always use GPU; fail if unavailable or unsuccessful +}; + +/// Selects the preconditioner used by the GPU BiCGSTAB solver. +/// Jacobi matches the CPU solver's preconditioner and is the default. ILU0 is +/// experimental because cuSPARSE SpSV value-analysis behavior differs between +/// CUDA versions and can introduce structured solver error if stale. +enum class GpuPreconditioner { Jacobi, ILU0 }; + +/// Parameters for the steady oxidant diffusion model used by +/// OxidationDiffusion. +struct OxidationParameters { + double diffusionCoefficient = 1.; + double reactionRate = 1.; + double transferCoefficient = 1.; + double equilibriumConcentration = 1.; + double oxidantMoleculeDensity = 1.; + double expansionCoefficient = 1.; + double velocitySign = 1.; + + // Stress coupling for reaction rate: + // k_eff = k * exp(-(p - p_ref) * V_k / (k_B * T)). + // Activation volumes are in m^3, pressure is in Pa, temperature is in K. + double temperature = 1273.15; + double reactionActivationVolume = 0.; + double referencePressure = 0.; + + // Stress coupling for diffusion coefficient: + // D_eff = D * exp(-(p - p_ref) * V_D / (k_B * T)). + double diffusionActivationVolume = 0.; + + // Crystal orientation factor on reaction rate. + // k(n) = k * [1 + (reactionRateRatio111 - 1) * (1 - (n . crystalAxis)^2)] + // reactionRateRatio111 = 1 disables the correction (isotropic). + double reactionRateRatio111 = 1.; + Vec3D crystalAxis = {0., 1., 0.}; + + double maskTransferCoefficient = 0.; + double maskConcentration = 0.; + double minBoundaryDistance = 1e-6; + unsigned maxIterations = 10000; + double tolerance = 1e-8; + double relaxation = 1.; + std::size_t maxGridPoints = 5000000; + int material = -1; +}; + +/// Solves the oxidant diffusion step of the Suvorov et al. +/// (10.1007/s10825-006-0003-z) oxidation model on the Cartesian grid carrying +/// two level sets. +/// +/// The oxide is assumed to lie between the Si/SiO2 reaction interface and the +/// SiO2/O2 ambient interface. Regular Cartesian grid nodes inside this band are +/// solved with finite differences. Whenever an axis-aligned grid edge leaves +/// the oxide, a cross-point is inserted at the zero of the crossed level-set +/// function and a Robin boundary condition is applied there. The diffusion +/// update uses the resulting sub-grid distances in a nonuniform three-point +/// stencil along each Cartesian axis. +/// +/// The default sign convention matches the multilayer convention described in +/// the paper: oxide is above the reaction interface and below the ambient +/// interface, i.e. reactionPhi >= 0 and ambientPhi <= 0. +template +class OxidationDiffusion final : public VelocityField, + public OxidationSolverBase { + using IndexType = viennahrle::Index; + using ConstSparseIterator = + viennahrle::ConstSparseIterator::DomainType>; + +private: + static constexpr T boltzmannConstant = T(1.380649e-23); + static constexpr T minStressFactor = T(1.e-6); + static constexpr T maxStressFactor = T(1.e6); + + // bring base members into scope + using OxidationSolverBase::noNode; + using OxidationSolverBase::nodeLookupFlat; + using OxidationSolverBase::initNodeLookup; + using OxidationSolverBase::lookupNode; + using OxidationSolverBase::minIndex; + using OxidationSolverBase::maxIndex; + using OxidationSolverBase::extents; + using OxidationSolverBase::strides; + using OxidationSolverBase::gridDelta; + using OxidationSolverBase::crosses; + using OxidationSolverBase::valueAt; + using OxidationSolverBase::inBounds; + using OxidationSolverBase::linearIndex; + using OxidationSolverBase::increment; + using OxidationSolverBase::findNearbyNode; + using OxidationSolverBase::initializeGridFromInterfaces; + + enum class Boundary { NONE, REACTION, AMBIENT, MASK }; + + struct Node { + IndexType index; + T concentration = 0.; + Vec3D siNormal = {0., 0., + 0.}; // unit outward normal of Si surface (into oxide) + }; + + struct StencilSide { + T distance = 1.; + T nodeCoefficient = 1.; + T constant = 0.; + }; + + SmartPointer> reactionInterface = nullptr; + SmartPointer> ambientInterface = nullptr; + SmartPointer> maskInterface = nullptr; + OxidationParameters parameters; + int reactionSign = 1; + int ambientSign = -1; + int maskSign = 1; + IndexType requestedMinIndex{}; + IndexType requestedMaxIndex{}; + unsigned iterations = 0; + T residual = std::numeric_limits::max(); + T normalizedResidual_ = std::numeric_limits::max(); + bool lastSolveConverged_ = false; + T maxScalarVelocity_ = 0.; + bool solved = false; + bool nodesDirty_ = true; // true β†’ rebuild grid/nodes on next apply() + mutable std::string + lastLoggedBackend_; // suppresses repeated "using X" messages + bool useRequestedBounds = false; + bool warmStartable_ = + false; // true when nodes[i].concentration holds a prior solution + std::unordered_map pressureLookup; + std::unordered_map concentrationCache_; + std::vector nodes; + // Face-major flat BC arrays: index = fi * n + nodeId, where fi in [0, 2*D). + // Face-major layout gives coalesced GPU reads when all warp threads access + // the same face of consecutive nodes. + std::vector faceBCTypes_; + std::vector faceBCDists_; + // Neighbor node IDs for every face (geometry-only, rebuilt with nodes). + // Entry == noNode for boundary / out-of-bounds faces. + std::vector neighborIds_; + + GpuMode gpuMode_ = GpuMode::Cpu; + GpuPreconditioner gpuPreconditioner_ = GpuPreconditioner::Jacobi; + +#ifdef VIENNALS_GPU_BICGSTAB + // Opaque pointer to device-side buffers (GpuBiCGSTABBuffers is defined only + // in the CUDA translation unit; we hold a raw pointer here so g++ never sees + // the CUDA internals). + gpu::GpuBiCGSTABBuffers *gpuBufs_ = nullptr; +#endif + +public: + /// Sub-grid accurate sample of the reaction boundary crossing closest to a + /// given grid node. Carries the interpolated concentration at the boundary + /// point, plus the axis and offset of the edge on which the crossing lies so + /// that callers can reconstruct the boundary-point position and normal. + struct ReactionBoundarySample { + bool found = false; + IndexType nodeIndex{}; + T distance = 1.; + T concentration = 0.; + unsigned crossingAxis = 0; // Cartesian axis of the crossing edge + int crossingOffset = 0; // +1 or -1: which neighbour the crossing faces + }; + + OxidationDiffusion() = default; + + OxidationDiffusion(SmartPointer> passedReactionInterface, + SmartPointer> passedAmbientInterface, + OxidationParameters passedParameters = {}) + : reactionInterface(passedReactionInterface), + ambientInterface(passedAmbientInterface), parameters(passedParameters) { + } + + ~OxidationDiffusion() { +#ifdef VIENNALS_GPU_BICGSTAB + gpu::freeGpuBuffers(gpuBufs_); + gpuBufs_ = nullptr; +#endif + } + + template static auto New(Args &&...args) { + return SmartPointer::New(std::forward(args)...); + } + + /// Call after any in-place modification of the level sets (e.g. after + /// ls::Advect) so that the next apply() rebuilds the Cartesian node grid. + void markGeometryChanged() { + nodesDirty_ = true; + solved = false; + } + + void setReactionInterface(SmartPointer> passedInterface) { + reactionInterface = passedInterface; + nodesDirty_ = true; + solved = false; + } + + void setAmbientInterface(SmartPointer> passedInterface) { + ambientInterface = passedInterface; + nodesDirty_ = true; + solved = false; + } + + void setMaskInterface(SmartPointer> passedInterface, + int passedMaskSign = 1) { + maskInterface = passedInterface; + maskSign = (passedMaskSign < 0) ? -1 : 1; + nodesDirty_ = true; + solved = false; + } + + void clearMaskInterface() { + maskInterface = nullptr; + nodesDirty_ = true; + solved = false; + } + + void setParameters(OxidationParameters passedParameters) { + parameters = passedParameters; + solved = false; + } + + OxidationParameters getParameters() const { return parameters; } + + T getEffectiveReactionRate(const Vec3D &coordinate) const { + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + return getEffectiveReactionRate(index); + } + + void clearPressureField() { + pressureLookup.clear(); + solved = false; + } + + void setPressure(const IndexType &index, T pressure) { + const auto key = detail::gridIndexHash(index); + if (std::isfinite(pressure)) + pressureLookup[key] = pressure; + else + pressureLookup.erase(key); + solved = false; + } + + void setPressure(const Vec3D &coordinate, T pressure) { + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + setPressure(index, pressure); + } + + /// Set signs defining the oxide band. A node is inside oxide if + /// reactionSign * reactionPhi >= 0 and ambientSign * ambientPhi >= 0. + void setOxideSigns(int passedReactionSign, int passedAmbientSign) { + reactionSign = (passedReactionSign < 0) ? -1 : 1; + ambientSign = (passedAmbientSign < 0) ? -1 : 1; + solved = false; + } + + /// Restrict the dense Cartesian diffusion solve to a finite index box. + /// This is useful for level sets with infinite boundary conditions. + void setSolveBounds(const IndexType &passedMinIndex, + const IndexType &passedMaxIndex) { + requestedMinIndex = passedMinIndex; + requestedMaxIndex = passedMaxIndex; + useRequestedBounds = true; + nodesDirty_ = true; + solved = false; + } + + void clearSolveBounds() { + useRequestedBounds = false; + nodesDirty_ = true; + solved = false; + } + + void apply() { + if (reactionInterface == nullptr || ambientInterface == nullptr) { + Logger::getInstance() + .addError("OxidationDiffusion: Missing level-set " + "interface.") + .print(); + return; + } + + if (nodesDirty_) { + if (!initialiseGrid()) + return; // base class already logged the error + buildNodes(); + nodesDirty_ = false; + if (nodes.empty()) + Logger::getInstance() + .addWarning("OxidationDiffusion: no oxide nodes found after " + "buildNodes(). Verify that the reaction and ambient " + "level sets enclose a non-empty oxide band.") + .print(); + } + solveDiffusion(); + + concentrationCache_.clear(); + for (const auto &node : nodes) + concentrationCache_[detail::gridIndexHash(node.index)] = + node.concentration; + + maxScalarVelocity_ = 0.; + ConstSparseIterator reactionIt(reactionInterface->getDomain()); + for (const auto &node : nodes) { + const auto sample = reactionBoundarySampleFromNode(reactionIt, node); + if (!sample.found) + continue; + const T rate = getEffectiveReactionRate(node.index); + const T vel = + std::abs(parameters.velocitySign) * rate * sample.concentration / + (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient); + maxScalarVelocity_ = std::max(maxScalarVelocity_, vel); + } + + solved = true; + } + + T getScalarVelocity(const Vec3D &coordinate, int material, + const Vec3D &normalVector, + unsigned long /*pointId*/) final { + if (!solved) + apply(); + + if (parameters.material >= 0 && material != parameters.material) + return 0.; + + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + + const auto boundarySample = reactionBoundarySample(index); + const IndexType rateIndex = + boundarySample.found ? boundarySample.nodeIndex : index; + const T concentration = boundarySample.found + ? boundarySample.concentration + : getReactionBoundaryConcentration(index); + return parameters.velocitySign * getEffectiveReactionRate(rateIndex) * + concentration / + (parameters.oxidantMoleculeDensity * + parameters.expansionCoefficient); + } + + T getDissipationAlpha(int /*direction*/, int material, + const Vec3D & /*centralDifferences*/) final { + if (parameters.material >= 0 && material != parameters.material) + return 0.; + + T bulkVel = + std::abs(parameters.velocitySign) * parameters.reactionRate * + parameters.equilibriumConcentration / + (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient); + + return std::max(maxScalarVelocity_, bulkVel); + } + + T getConcentration(const Vec3D &coordinate) const { + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + return getConcentration(index); + } + + T getConcentration(const IndexType &index) const { + const std::size_t nodeId = lookupNode(index); + if (nodeId == noNode) { + const auto nearby = findNearbyNode(index); + if (nearby == noNode) + return 0.; + return nodes[nearby].concentration; + } + return nodes[nodeId].concentration; + } + + T getReactionBoundaryConcentration(const Vec3D &coordinate) const { + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + return getReactionBoundaryConcentration(index); + } + + T getReactionBoundaryConcentration(const IndexType &index) const { + const auto sample = reactionBoundarySample(index); + return sample.found ? sample.concentration : getConcentration(index); + } + + unsigned getIterations() const { return iterations; } + T getResidual() const { return residual; } + T getNormalizedResidual() const { return normalizedResidual_; } + bool lastSolveConverged() const { return lastSolveConverged_; } + std::size_t getNumberOfSolutionNodes() const { return nodes.size(); } + bool hasFiniteConcentrationField() const { + for (const auto &node : nodes) + if (!std::isfinite(node.concentration)) + return false; + return true; + } + + const std::unordered_map &getConcentrationCache() const { + return concentrationCache_; + } + + void setConcentrationCache(std::unordered_map cache) { + concentrationCache_ = std::move(cache); + } + + /// Set the GPU solver selection mode. See GpuMode for the two options. + /// On CPU-only builds (VIENNALS_GPU_BICGSTAB not defined) this is a no-op. + void setGpuMode(GpuMode mode) { gpuMode_ = mode; } + /// Set the GPU BiCGSTAB preconditioner. Jacobi matches the CPU solver. + void setGpuPreconditioner(GpuPreconditioner preconditioner) { + gpuPreconditioner_ = preconditioner; + } + + /// Mark the current solution as valid without re-solving. Call this before + /// any parallel advection (lsAdvect) that uses this field as a velocity + /// source to prevent concurrent apply() calls inside getScalarVelocity(). + void markSolved() { solved = true; } + + /// Write per-node concentration into ambientInterface->getPointData() so + /// that lsInterior + lsAdvect can carry it across timestep boundaries. + /// Safe to call regardless of the solved flag; uses the most recent nodes. + void writeConcentrationToLevelSet() { + if (nodes.empty() || ambientInterface == nullptr) + return; + + std::vector concentrations; + ConstSparseIterator it(ambientInterface->getDomain()); + for (; !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + const IndexType idx = it.getStartIndices(); + const std::size_t nodeId = lookupNode(idx); + // Non-solve-grid narrow-band points (mask interior, gas-phase boundary) + // get concentration 0. Using equilibriumConcentration here contaminates + // the output: mask-interior points are impermeable (Cβ‰ˆ0), and newly + // appeared bird's-beak points inherit C=1 spuriously after advection. + // The in-memory concentrationCache_ is the warm-start source for the + // next substep; the level-set value is only the fallback when that cache + // is cold, and C=0 converges just as quickly as C=1 from a cold start. + const T value = nodeId != noNode ? nodes[nodeId].concentration : T(0); + concentrations.push_back(std::isfinite(value) ? value : T(0)); + } + ambientInterface->getPointData().insertReplaceScalarData( + std::move(concentrations), "OxConcentration"); + } + + /// Write per-node pressure into ambientInterface->getPointData() so that + /// it survives advection and can warm-start the coupling loop next step. + void writePressureToLevelSet() { + if (pressureLookup.empty() || ambientInterface == nullptr) + return; + + std::vector pressures; + ConstSparseIterator it(ambientInterface->getDomain()); + for (; !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + const IndexType idx = it.getStartIndices(); + const auto pIt = pressureLookup.find(detail::gridIndexHash(idx)); + const T value = pIt != pressureLookup.end() ? pIt->second : T(0); + pressures.push_back(std::isfinite(value) ? value : T(0)); + } + ambientInterface->getPointData().insertReplaceScalarData( + std::move(pressures), "OxPressure"); + } + + /// Convenience wrapper: persist both concentration and pressure in one call. + void writePersistentFields() { + writeConcentrationToLevelSet(); + writePressureToLevelSet(); + } + + /// Return the reaction boundary sample for the grid node nearest to + /// `coordinate`. Used by the deformation solver to get both the sub-grid + /// concentration and the crossing edge so it can compute a sub-grid normal. + ReactionBoundarySample + getReactionBoundarySample(const Vec3D &coordinate) const { + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + return reactionBoundarySample(index); + } + + /// Absolute scalar velocity derived from an already-computed boundary sample, + /// avoiding a second call to reactionBoundarySample inside getScalarVelocity. + T getScalarVelocityFromSample(const ReactionBoundarySample &sample) const { + if (!sample.found) + return T(0); + return std::abs( + getEffectiveReactionRate(sample.nodeIndex) * sample.concentration / + (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient)); + } + +private: + bool initialiseGrid() { + return initializeGridFromInterfaces( + reactionInterface, ambientInterface, maskInterface, useRequestedBounds, + requestedMinIndex, requestedMaxIndex, parameters.maxGridPoints, + "OxidationDiffusion"); + } + + void buildNodes() { + nodes.clear(); + initNodeLookup(); + + // On the first substep after an outer-step boundary the in-memory cache is + // empty. Fall back to the concentration stored in the level set's pointData + // (written by writeConcentrationToLevelSet() before the previous advection + // and remapped by lsAdvect + lsInterior). + warmStartable_ = !concentrationCache_.empty(); + if (!warmStartable_) { + // Single HRLE pass restores both concentration and pressure from the + // pointData written by writePersistentFields() before the last advection. + const int cIdx = ambientInterface->getPointData().getScalarDataIndex( + "OxConcentration"); + const int pIdx = + ambientInterface->getPointData().getScalarDataIndex("OxPressure"); + const auto *cd = + (cIdx != -1) ? ambientInterface->getPointData().getScalarData(cIdx) + : nullptr; + const auto *pd = + (pIdx != -1) ? ambientInterface->getPointData().getScalarData(pIdx) + : nullptr; + if (cd != nullptr || pd != nullptr) { + ConstSparseIterator it(ambientInterface->getDomain()); + for (; !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + const auto ptId = it.getPointId(); + const std::size_t key = + detail::gridIndexHash(it.getStartIndices()); + if (cd != nullptr && ptId < static_cast(cd->size()) && + std::isfinite((*cd)[ptId])) + concentrationCache_[key] = (*cd)[ptId]; + if (pd != nullptr && ptId < static_cast(pd->size()) && + std::isfinite((*pd)[ptId])) + pressureLookup[key] = (*pd)[ptId]; + } + warmStartable_ = !concentrationCache_.empty(); + } + } + + ConstSparseIterator reactionIt(reactionInterface->getDomain()); + ConstSparseIterator ambientIt(ambientInterface->getDomain()); + auto maskIt = makeMaskIterator(); + + IndexType index = minIndex; + while (true) { + const T reactionPhi = valueAt(reactionIt, index); + const T ambientPhi = valueAt(ambientIt, index); + if (isInsideOxide(reactionPhi, ambientPhi) && + !isInsideMask(maskIt, index)) { + const std::size_t id = nodes.size(); + nodeLookupFlat[linearIndex(index)] = id; + T seedConc = parameters.equilibriumConcentration; + auto cacheIt = + concentrationCache_.find(detail::gridIndexHash(index)); + if (cacheIt != concentrationCache_.end()) + seedConc = cacheIt->second; + if (!std::isfinite(seedConc)) + seedConc = parameters.equilibriumConcentration; + Node newNode{index, seedConc}; + if (parameters.reactionRateRatio111 != T(1)) + newNode.siNormal = computeSiNormal(index, reactionIt); + nodes.push_back(newNode); + } + + if (!increment(index)) + break; + } + + // Precompute per-face boundary intersections into flat face-major arrays. + // Level-set positions are fixed during the inner solve; one pass per + // apply(). + const std::size_t n = nodes.size(); + faceBCTypes_.assign(2 * D * n, Boundary::NONE); + faceBCDists_.assign(2 * D * n, T(1)); + ConstSparseIterator faceReactionIt(reactionInterface->getDomain()); + ConstSparseIterator faceAmbientIt(ambientInterface->getDomain()); + auto faceMaskIt = makeMaskIterator(); + for (std::size_t id = 0; id < n; ++id) { + const auto &node = nodes[id]; + for (unsigned dir = 0; dir < D; ++dir) { + for (int off : {-1, 1}) { + const unsigned fi = dir * 2u + (off == 1 ? 1u : 0u); + IndexType nb = node.index; + nb[dir] += off; + if (!inBounds(nb) || lookupNode(nb) != noNode) + continue; // NONE/1.0 already set by assign() + const auto bc = classifyBoundary(faceReactionIt, faceAmbientIt, + faceMaskIt, node.index, nb); + faceBCTypes_[fi * n + id] = bc.first; + faceBCDists_[fi * n + id] = bc.second; + } + } + } + + // Precompute per-face neighbor node IDs (geometry-only). + // Used by computeFaceCoeffs() and uploaded once to GPU. + neighborIds_.assign(2 * D * n, noNode); + for (std::size_t id = 0; id < n; ++id) { + for (unsigned dir = 0; dir < D; ++dir) { + for (int off : {-1, 1}) { + const unsigned fi = dir * 2u + (off == 1 ? 1u : 0u); + IndexType nb = nodes[id].index; + nb[dir] += off; + if (inBounds(nb)) + neighborIds_[fi * n + id] = lookupNode(nb); + } + } + } + + bool loggedBackend = false; +#ifdef VIENNALS_GPU_BICGSTAB + // Free any stale allocation from a previous buildNodes() call. + gpu::freeGpuBuffers(gpuBufs_); + gpuBufs_ = nullptr; + + const bool tryGpu = (gpuMode_ == GpuMode::Gpu); + if (tryGpu) { + const bool useIlu0 = gpuPreconditioner_ == GpuPreconditioner::ILU0; + gpuBufs_ = gpu::allocGpuBuffers(static_cast(n), 2 * D, useIlu0); + + if (gpuBufs_) { + // Convert std::size_t neighbor IDs to uint32_t for the device + const std::size_t nf = 2u * D * n; + std::vector nb32(nf); + for (std::size_t k = 0; k < nf; ++k) + nb32[k] = (neighborIds_[k] == noNode) + ? gpu::kNoNode + : static_cast(neighborIds_[k]); + if (!gpu::gpuUploadNeighborIds(gpuBufs_, nb32.data(), nf)) { + gpu::freeGpuBuffers(gpuBufs_); + gpuBufs_ = nullptr; + VIENNACORE_LOG_ERROR("OxidationDiffusion: GPU mode was selected, but " + "uploading GPU neighbor IDs failed." + + gpuErrorDetail()); + } + if (useIlu0 && !gpu::gpuSetupCSR(gpuBufs_, nb32.data(), + static_cast(n), 2 * D)) { + gpu::freeGpuBuffers(gpuBufs_); + gpuBufs_ = nullptr; + VIENNACORE_LOG_ERROR("OxidationDiffusion: GPU mode was selected, but " + "CUSPARSE setup for the GPU BiCGSTAB solver " + "failed." + + gpuErrorDetail()); + } + logDiffusionBackend("GPU BiCGSTAB", + "preconditioner=" + + std::string(useIlu0 ? "ILU0" : "Jacobi")); + loggedBackend = true; + } else { + VIENNACORE_LOG_ERROR("OxidationDiffusion: GPU mode was selected, but " + "CUDA buffers could not be " + "allocated or the CUDA context could not be " + "initialized." + + gpuErrorDetail()); + } + } +#endif + if (!loggedBackend) { +#ifdef VIENNALS_GPU_BICGSTAB + logDiffusionBackend("CPU BiCGSTAB", "GPU mode not selected"); +#else + logDiffusionBackend("CPU BiCGSTAB", + "ViennaLS was built without GPU BiCGSTAB support"); +#endif + } + } + + // Precompute per-face coupling coefficients for the GPU SpMV. + // + // faceCoeffs[fi * n + id] = 2*D_eff / (dist_fi * distSum_axis) + // + // Only interior faces (neighbor != noNode) get a nonzero entry; + // boundary and out-of-bounds faces contribute to diag/b but not to + // the off-diagonal coupling stored here. Must be called after diag/b + // are computed (or at least after D_eff is known), because D_eff + // depends on pressure when diffusionActivationVolume != 0. + void computeFaceCoeffs(std::vector &faceCoeffs) const { + const std::size_t n = nodes.size(); + faceCoeffs.assign(2 * D * n, T(0)); + const T eps = std::numeric_limits::epsilon(); + const std::vector zeros(n, T(0)); + + for (std::size_t id = 0; id < n; ++id) { + const T D_eff = getEffectiveDiffusionCoefficient(nodes[id].index); + for (unsigned dir = 0; dir < D; ++dir) { + const unsigned fiNeg = dir * 2u; + const unsigned fiPos = dir * 2u + 1u; + + // Use the exact side construction used by the CPU stencil so the GPU + // SpMV cannot drift from computeStencilAt() at cut-cell boundaries. + const auto neg = makeStencilSide(id, zeros, dir, -1, D_eff); + const auto pos = makeStencilSide(id, zeros, dir, 1, D_eff); + const T distNeg = neg.distance; + const T distPos = pos.distance; + const T distSum = distNeg + distPos; + if (distSum <= eps) + continue; + + if (neighborIds_[fiNeg * n + id] != noNode && distNeg > eps) + faceCoeffs[fiNeg * n + id] = T(2) * D_eff / (distNeg * distSum); + if (neighborIds_[fiPos * n + id] != noNode && distPos > eps) + faceCoeffs[fiPos * n + id] = T(2) * D_eff / (distPos * distSum); + } + } + } + + // Evaluates the stencil at one node: fills diag = A[i,i] and + // rhs = sum_j(A_off[i,j] * x[j]) + bc_constants[i]. + // Called with x = zeros to precompute the geometry-fixed diagonal and b. + template + void computeStencilAt(std::size_t nodeId, const std::vector &x, + T &diag, T &rhs) const { + diag = T(0); + rhs = T(0); + const T D_eff = getEffectiveDiffusionCoefficient(nodes[nodeId].index); + for (unsigned direction = 0; direction < D; ++direction) { + const auto neg = makeStencilSide(nodeId, x, direction, -1, D_eff); + const auto pos = makeStencilSide(nodeId, x, direction, 1, D_eff); + addAxisContribution(rhs, diag, neg, pos, D_eff); + } + } + + // Computes Av = A * v using precomputed diagonal and b (RHS constants). + // (Av)[i] = precomputedDiag[i]*v[i] - rhs_at_v[i] + b[i] + // Stencil arithmetic stays in T; only storage uses SolverT. + template + void matvec(const std::vector &v, + const std::vector &precomputedDiag, const std::vector &b, + std::vector &Av) const { +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < nodes.size(); ++i) { + T diag, rhs; + computeStencilAt(i, v, diag, rhs); + Av[i] = static_cast(precomputedDiag[i] * v[i] - rhs + b[i]); + } + } + + void solveDiffusion() { + iterations = 0; + residual = 0.; + normalizedResidual_ = 0.; + lastSolveConverged_ = false; + if (nodes.empty()) { + lastSolveConverged_ = true; + return; + } + + // Work vectors use float to halve memory bandwidth in the SpMV hot path. + // Stencil arithmetic and dot-product accumulation remain in T (double). + using SolverT = float; + + const std::size_t n = nodes.size(); + const T eps = std::numeric_limits::epsilon(); + + // Geometry-fixed diagonal and BC source vector (kept in T for full + // precision). + Timer<> tDiag; + tDiag.start(); + std::vector diag(n), b(n); + { + const std::vector zeros(n, SolverT(0)); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) + computeStencilAt(i, zeros, diag[i], b[i]); + } + tDiag.finish(); + + T b_norm = T(0); + bool finiteSystem = true; + for (std::size_t i = 0; i < n; ++i) { + finiteSystem = + finiteSystem && std::isfinite(diag[i]) && std::isfinite(b[i]); + b_norm = std::max(b_norm, std::abs(b[i])); + } + if (b_norm < T(1e-100)) + b_norm = T(1); + if (!finiteSystem) { + residual = std::numeric_limits::infinity(); + normalizedResidual_ = residual; + Logger::getInstance() + .addWarning("solveDiffusion: assembled non-finite matrix/RHS; " + "rejecting this coupled trial.") + .print(); + return; + } + + // Initial guess: warm-start or diagonal-preconditioned b. Sanitize the + // warm start so a previous failed solve cannot seed NaNs into the next one. + auto fallbackInitialGuess = [&](std::size_t i) -> T { + if (std::isfinite(diag[i]) && std::isfinite(b[i]) && diag[i] > eps) { + const T guess = b[i] / diag[i]; + if (std::isfinite(guess)) + return guess; + } + return T(0); + }; + + std::vector x(n); + for (std::size_t i = 0; i < n; ++i) { + T guess = + warmStartable_ ? nodes[i].concentration : fallbackInitialGuess(i); + if (!std::isfinite(guess)) + guess = fallbackInitialGuess(i); + x[i] = static_cast(guess); + if (!std::isfinite(x[i])) + x[i] = SolverT(0); + } + const auto initialGuess = x; + +#ifdef VIENNALS_GPU_BICGSTAB + // ── GPU BiCGSTAB path ────────────────────────────────────────────── + // Engaged when GpuMode::Gpu is set and buildNodes() allocated GPU buffers. + // Uploads diag, b, and face coefficients fresh each call (they are + // pressure-dependent via D_eff), then runs the full BiCGSTAB loop on + // the device. Only dot-product scalars and the max-abs residual are + // downloaded; work vectors stay on the GPU between iterations. + if (gpu::gpuIsValid(gpuBufs_)) { + Timer<> tPrep, tUpload, tSolve; + tPrep.start(); + std::vector diagGpu(n), bGpu(n); + for (std::size_t i = 0; i < n; ++i) { + diagGpu[i] = static_cast(diag[i]); + bGpu[i] = static_cast(b[i]); + } + + // Face coupling coefficients (pressure-dependent; recomputed each call) + std::vector faceCoeffs; + computeFaceCoeffs(faceCoeffs); + std::vector coeffGpu(faceCoeffs.size()); + for (std::size_t k = 0; k < faceCoeffs.size(); ++k) + coeffGpu[k] = static_cast(faceCoeffs[k]); + + std::vector xGpu(n); + for (std::size_t i = 0; i < n; ++i) + xGpu[i] = static_cast(x[i]); + tPrep.finish(); + + tUpload.start(); + const bool gpuUploadOk = gpu::gpuUploadSolverArrays( + gpuBufs_, diagGpu.data(), bGpu.data(), coeffGpu.data(), + static_cast(n), faceCoeffs.size()); + tUpload.finish(); + if (!gpuUploadOk) { + if (Logger::hasDebug()) { + const std::string tag = + "diffusion n=" + std::to_string(n) + " [GPU upload failed]"; + Logger::getInstance() + .addTiming(tag + " diag/b precompute", tDiag) + .addTiming(tag + " GPU prep+faceCoeffs", tPrep) + .addTiming(tag + " GPU upload", tUpload) + .print(); + } + VIENNACORE_LOG_ERROR("OxidationDiffusion: GPU mode was selected, but " + "uploading GPU solver arrays or factorizing ILU " + "failed." + + gpuErrorDetail()); + } + + // Solve on GPU; xGpu holds the initial guess and receives the solution. + tSolve.start(); + const bool gpuConverged = gpu::gpuSolveBiCGSTAB( + gpuBufs_, xGpu.data(), static_cast(eps), + parameters.maxIterations, static_cast(parameters.tolerance), + iterations, residual); + tSolve.finish(); + + const bool finiteGpuSolution = + gpuConverged && + std::all_of(xGpu.begin(), xGpu.end(), + [](double value) { return std::isfinite(value); }); + const unsigned gpuIterations = iterations; + const T gpuResidual = residual; + + if (finiteGpuSolution) { + // Write solution back to nodes only after convergence and finite + // checks. + for (std::size_t i = 0; i < n; ++i) + nodes[i].concentration = static_cast(xGpu[i]); + normalizedResidual_ = gpuResidual / b_norm; + lastSolveConverged_ = std::isfinite(normalizedResidual_) && + normalizedResidual_ <= parameters.tolerance; + + if (Logger::hasTiming()) { + const std::string tag = "diffusion n=" + std::to_string(n) + + " iters=" + std::to_string(iterations) + + " res=" + std::to_string(residual) + " [GPU]"; + Logger::getInstance() + .addTiming(tag + " GPU BiCGSTAB", tSolve) + .print(); + } + if (Logger::hasDebug()) { + const std::string tag = "diffusion n=" + std::to_string(n) + " [GPU]"; + Logger::getInstance() + .addTiming(tag + " diag/b precompute", tDiag) + .addTiming(tag + " GPU prep+faceCoeffs", tPrep) + .addTiming(tag + " GPU upload", tUpload) + .print(); + } + return; + } + + if (Logger::hasDebug()) { + const std::string tag = "diffusion n=" + std::to_string(n) + + " iters=" + std::to_string(gpuIterations) + + " res=" + std::to_string(gpuResidual) + + " [GPU failed]"; + Logger::getInstance() + .addTiming(tag + " diag/b precompute", tDiag) + .addTiming(tag + " GPU prep+faceCoeffs", tPrep) + .addTiming(tag + " GPU upload", tUpload) + .addTiming(tag + " GPU BiCGSTAB", tSolve) + .print(); + } + VIENNACORE_LOG_ERROR( + "OxidationDiffusion: GPU mode was selected, but GPU BiCGSTAB " + "failed, did not converge, or produced non-finite concentrations " + "(iters=" + + std::to_string(gpuIterations) + + ", residual=" + std::to_string(gpuResidual) + ")."); + } +#else + if (gpuMode_ == GpuMode::Gpu) { + VIENNACORE_LOG_ERROR("OxidationDiffusion: explicit GPU mode was " + "requested, but ViennaLS was built without " + "VIENNALS_GPU_BICGSTAB."); + } +#endif + + // r = b - A*x + std::vector Ax(n); + matvec(x, diag, b, Ax); + std::vector r(n), r_hat(n); + for (std::size_t i = 0; i < n; ++i) { + r[i] = static_cast(b[i] - Ax[i]); + r_hat[i] = r[i]; + } + + // BiCGSTAB with diagonal (Jacobi) preconditioner. + // Scalars (rho, alpha, omega, beta) and dot products stay in T for + // stability. + Timer<> tCpuSolve; + tCpuSolve.start(); + std::vector p(n, SolverT(0)), v(n, SolverT(0)), y(n), z(n), s(n), + t(n); + T rho = T(1), alpha = T(1), omega = T(1); + bool bicgstabBreakdown = false; + for (std::size_t i = 0; i < n; ++i) { + const T ri = static_cast(r[i]); + if (!std::isfinite(ri)) { + bicgstabBreakdown = true; + break; + } + residual = std::max(residual, std::abs(ri)); + } + + for (; !bicgstabBreakdown && iterations < parameters.maxIterations; + ++iterations) { + T rho_new = T(0); + for (std::size_t i = 0; i < n; ++i) + rho_new += static_cast(r_hat[i]) * static_cast(r[i]); + + if (!std::isfinite(rho_new)) { + bicgstabBreakdown = true; + break; + } + if (std::abs(rho_new) < T(1e-100)) + break; + if (!std::isfinite(rho) || !std::isfinite(alpha) || + !std::isfinite(omega) || std::abs(omega) < T(1e-100)) { + bicgstabBreakdown = true; + break; + } + + const T beta = (rho_new / rho) * (alpha / omega); + if (!std::isfinite(beta)) { + bicgstabBreakdown = true; + break; + } + rho = rho_new; + + for (std::size_t i = 0; i < n; ++i) + p[i] = static_cast(r[i] + beta * (p[i] - omega * v[i])); + + for (std::size_t i = 0; i < n; ++i) { + const T pi = p[i]; + y[i] = static_cast((diag[i] > eps) ? pi / diag[i] : pi); + } + + matvec(y, diag, b, v); + + T r_hat_v = T(0); + for (std::size_t i = 0; i < n; ++i) + r_hat_v += static_cast(r_hat[i]) * static_cast(v[i]); + if (!std::isfinite(r_hat_v)) { + bicgstabBreakdown = true; + break; + } + if (std::abs(r_hat_v) < T(1e-100)) + break; + + alpha = rho_new / r_hat_v; + if (!std::isfinite(alpha)) { + bicgstabBreakdown = true; + break; + } + + for (std::size_t i = 0; i < n; ++i) + s[i] = static_cast(r[i] - alpha * v[i]); + + residual = T(0); + for (std::size_t i = 0; i < n; ++i) + residual = std::max(residual, std::abs(static_cast(s[i]))); + if (!std::isfinite(residual)) { + bicgstabBreakdown = true; + break; + } + if (residual < parameters.tolerance * b_norm) { + for (std::size_t i = 0; i < n; ++i) + x[i] = static_cast(x[i] + alpha * y[i]); + ++iterations; + break; + } + + for (std::size_t i = 0; i < n; ++i) { + const T si = s[i]; + z[i] = static_cast((diag[i] > eps) ? si / diag[i] : si); + } + + matvec(z, diag, b, t); + + T t_s = T(0), t_t = T(0); + for (std::size_t i = 0; i < n; ++i) { + t_s += static_cast(t[i]) * static_cast(s[i]); + t_t += static_cast(t[i]) * static_cast(t[i]); + } + if (!std::isfinite(t_s) || !std::isfinite(t_t)) { + bicgstabBreakdown = true; + break; + } + omega = (t_t > T(1e-100)) ? t_s / t_t : T(0); + if (!std::isfinite(omega)) { + bicgstabBreakdown = true; + break; + } + + for (std::size_t i = 0; i < n; ++i) { + x[i] = static_cast(x[i] + alpha * y[i] + omega * z[i]); + r[i] = static_cast(s[i] - omega * t[i]); + } + + residual = T(0); + for (std::size_t i = 0; i < n; ++i) + residual = std::max(residual, std::abs(static_cast(r[i]))); + if (!std::isfinite(residual)) { + bicgstabBreakdown = true; + break; + } + if (residual < parameters.tolerance * b_norm) { + ++iterations; + break; + } + } + + if (bicgstabBreakdown) + residual = std::numeric_limits::infinity(); + + const bool finiteCpuSolution = + !bicgstabBreakdown && + std::all_of(x.begin(), x.end(), + [](SolverT value) { return std::isfinite(value); }); + if (finiteCpuSolution) { + for (std::size_t i = 0; i < n; ++i) + nodes[i].concentration = x[i]; + } else { + for (std::size_t i = 0; i < n; ++i) + nodes[i].concentration = initialGuess[i]; + residual = std::numeric_limits::infinity(); + Logger::getInstance() + .addWarning("solveDiffusion: CPU BiCGSTAB produced non-finite " + "concentrations; keeping the sanitized initial guess.") + .print(); + } + normalizedResidual_ = residual / b_norm; + lastSolveConverged_ = finiteCpuSolution && + std::isfinite(normalizedResidual_) && + normalizedResidual_ <= parameters.tolerance; + + tCpuSolve.finish(); + if (Logger::hasTiming()) { + const std::string path = " [CPU]"; + const std::string tag = "diffusion n=" + std::to_string(n) + + " iters=" + std::to_string(iterations) + + " res=" + std::to_string(residual) + path; + Logger::getInstance().addTiming(tag + " CPU BiCGSTAB", tCpuSolve).print(); + } + if (Logger::hasDebug()) { + const std::string path = " [CPU]"; + Logger::getInstance() + .addTiming("diffusion n=" + std::to_string(n) + path + + " diag/b precompute", + tDiag) + .print(); + } + if (residual > parameters.tolerance * b_norm) + VIENNACORE_LOG_WARNING( + "solveDiffusion: BiCGSTAB did not converge after " + + std::to_string(iterations) + "/" + + std::to_string(parameters.maxIterations) + + " iterations (residual=" + std::to_string(residual / b_norm) + + ", tolerance=" + std::to_string(parameters.tolerance) + ")"); + } + +#ifdef VIENNALS_GPU_BICGSTAB + static std::string gpuErrorDetail() { + const char *detail = gpu::gpuGetLastErrorMessage(); + if (detail && detail[0] != '\0') + return std::string(" Detail: ") + detail; + return {}; + } +#endif + + void logDiffusionBackend(const std::string &backend, + const std::string &detail) const { + if (!Logger::hasInfo()) + return; + const std::string msg = + "OxidationDiffusion: using " + backend + + " for diffusion solve (nodes=" + std::to_string(nodes.size()) + + (detail.empty() ? std::string() : ", " + detail) + ")."; + if (msg == lastLoggedBackend_) + return; + lastLoggedBackend_ = msg; + Logger::getInstance().addInfo(msg).print(); + } + + // Uses precomputed flat faceBC arrays β€” no HRLE access, safe for parallel + // execution. + template + StencilSide + makeStencilSide(std::size_t nodeId, const std::vector &previous, + unsigned direction, int offset, T diffusion) const { + const IndexType &nodeIndex = nodes[nodeId].index; + IndexType neighbor = nodeIndex; + neighbor[direction] += offset; + + if (!inBounds(neighbor)) + return zeroFluxSide(); + + const std::size_t neighborId = lookupNode(neighbor); + if (neighborId != noNode) + return {gridDelta, 0., previous[neighborId]}; + + const unsigned fi = direction * 2u + (offset == 1 ? 1u : 0u); + const std::size_t n = nodes.size(); + const Boundary faceType = faceBCTypes_[fi * n + nodeId]; + const T faceDist = faceBCDists_[fi * n + nodeId]; + if (faceType == Boundary::REACTION) + return reactionBoundarySide(nodeIndex, faceDist, diffusion); + if (faceType == Boundary::AMBIENT) + return ambientBoundarySide(faceDist, diffusion); + if (faceType == Boundary::MASK) + return maskBoundarySide(faceDist, diffusion); + return zeroFluxSide(); + } + + void addAxisContribution(T &rightHandSide, T &diagonal, + const StencilSide &negativeSide, + const StencilSide &positiveSide, T diffusion) const { + const T distanceSum = negativeSide.distance + positiveSide.distance; + if (distanceSum <= std::numeric_limits::epsilon()) + return; + + addSideContribution(rightHandSide, diagonal, negativeSide, distanceSum, + diffusion); + addSideContribution(rightHandSide, diagonal, positiveSide, distanceSum, + diffusion); + } + + void addSideContribution(T &rightHandSide, T &diagonal, + const StencilSide &side, T distanceSum, + T diffusion) const { + if (side.distance <= std::numeric_limits::epsilon()) + return; + + const T coefficient = T(2) * diffusion / (side.distance * distanceSum); + rightHandSide += coefficient * side.constant; + diagonal += coefficient * (T(1) - side.nodeCoefficient); + } + + StencilSide zeroFluxSide() const { return {gridDelta, 1., 0.}; } + + StencilSide reactionBoundarySide(const IndexType &nodeIndex, T distance, + T diffusion) const { + const T conductance = diffusion / distance; + const T reactionRate = getEffectiveReactionRate(nodeIndex); + const T denominator = conductance + reactionRate; + if (denominator <= std::numeric_limits::epsilon()) + return zeroFluxSide(); + + return {distance, conductance / denominator, 0.}; + } + + ReactionBoundarySample reactionBoundarySample(const IndexType &index) const { + ConstSparseIterator reactionIt(reactionInterface->getDomain()); + + const std::size_t directId = lookupNode(index); + if (directId != noNode) { + const auto sample = + reactionBoundarySampleFromNode(reactionIt, nodes[directId]); + if (sample.found) + return sample; + } + + // The reaction velocity is a local interface quantity. A global nearest + // search can smear open-window concentrations deep under a mask if the + // local oxide band is temporarily missing or clipped near contact. Keep + // the fallback strictly local, consistent with the velocity-extension + // radius used elsewhere in the oxidation fields. + std::size_t bestNode = std::numeric_limits::max(); + T bestDistance2 = std::numeric_limits::max(); + + for (int radius = 1; radius <= 4; ++radius) { + IndexType offset{}; + offset.fill(-radius); + while (true) { + IndexType candidate = index; + T distance2 = 0.; + for (unsigned d = 0; d < D; ++d) { + candidate[d] += offset[d]; + distance2 += static_cast(offset[d] * offset[d]); + } + + if (distance2 > T(0) && inBounds(candidate)) { + const std::size_t foundId = nodeLookupFlat[linearIndex(candidate)]; + if (foundId != noNode && distance2 < bestDistance2) { + const auto sample = + reactionBoundarySampleFromNode(reactionIt, nodes[foundId]); + if (sample.found) { + bestDistance2 = distance2; + bestNode = foundId; + } + } + } + + unsigned dim = 0; + for (; dim < D; ++dim) { + if (offset[dim] < radius) { + ++offset[dim]; + break; + } + offset[dim] = -radius; + } + if (dim == D) + break; + } + + if (bestNode != std::numeric_limits::max()) + break; + } + + if (bestNode == std::numeric_limits::max()) + return {}; + return reactionBoundarySampleFromNode(reactionIt, nodes[bestNode]); + } + + ReactionBoundarySample + reactionBoundarySampleFromNode(ConstSparseIterator &reactionIt, + const Node &node) const { + ReactionBoundarySample best; + best.nodeIndex = node.index; + T bestDistance = std::numeric_limits::max(); + const T insidePhi = valueAt(reactionIt, node.index); + + for (unsigned direction = 0; direction < D; ++direction) { + for (const int offset : {-1, 1}) { + IndexType neighbor = node.index; + neighbor[direction] += offset; + if (!inBounds(neighbor)) + continue; + + const T outsidePhi = valueAt(reactionIt, neighbor); + if (!crosses(insidePhi, outsidePhi)) + continue; + + const T distance = crossingDistance(insidePhi, outsidePhi); + if (distance >= bestDistance) + continue; + + const T diffusion = getEffectiveDiffusionCoefficient(node.index); + const auto side = reactionBoundarySide(node.index, distance, diffusion); + best.found = true; + best.distance = distance; + best.concentration = + side.nodeCoefficient * node.concentration + side.constant; + best.crossingAxis = direction; + best.crossingOffset = offset; + bestDistance = distance; + } + } + + return best; + } + + StencilSide ambientBoundarySide(T distance, T diffusion) const { + const T conductance = diffusion / distance; + const T denominator = conductance + parameters.transferCoefficient; + if (denominator <= std::numeric_limits::epsilon()) + return zeroFluxSide(); + + return {distance, conductance / denominator, + parameters.transferCoefficient * + parameters.equilibriumConcentration / denominator}; + } + + StencilSide maskBoundarySide(T distance, T diffusion) const { + if (parameters.maskTransferCoefficient <= std::numeric_limits::epsilon()) + return zeroFluxSide(); + + const T conductance = diffusion / distance; + const T denominator = conductance + parameters.maskTransferCoefficient; + if (denominator <= std::numeric_limits::epsilon()) + return zeroFluxSide(); + + return {distance, conductance / denominator, + parameters.maskTransferCoefficient * parameters.maskConcentration / + denominator}; + } + + T getEffectiveReactionRate(const IndexType &index) const { + T rate = parameters.reactionRate; + + if (parameters.reactionActivationVolume != T(0)) { + T pressure = parameters.referencePressure; + const auto foundPressure = + pressureLookup.find(detail::gridIndexHash(index)); + if (foundPressure != pressureLookup.end()) + pressure = foundPressure->second; + if (!std::isfinite(pressure)) + pressure = parameters.referencePressure; + const T exponent = + stressExponent(pressure, parameters.reactionActivationVolume); + rate *= stressFactor(exponent); + } + + if (parameters.reactionRateRatio111 != T(1)) { + const std::size_t nodeId = lookupNode(index); + if (nodeId != noNode) { + const auto &normal = nodes[nodeId].siNormal; + T dot = T(0); + for (unsigned d = 0; d < D; ++d) + dot += normal[d] * parameters.crystalAxis[d]; + rate *= T(1) + + (parameters.reactionRateRatio111 - T(1)) * (T(1) - dot * dot); + } + } + + return rate; + } + + T getEffectiveDiffusionCoefficient(const IndexType &index) const { + if (parameters.diffusionActivationVolume == T(0)) + return parameters.diffusionCoefficient; + + T pressure = parameters.referencePressure; + const auto found = pressureLookup.find(detail::gridIndexHash(index)); + if (found != pressureLookup.end()) + pressure = found->second; + if (!std::isfinite(pressure)) + pressure = parameters.referencePressure; + + const T exponent = + stressExponent(pressure, parameters.diffusionActivationVolume); + return parameters.diffusionCoefficient * stressFactor(exponent); + } + + T stressExponent(T pressure, T activationVolume) const { + const T thermalEnergy = + boltzmannConstant * std::max(parameters.temperature, T(1.)); + return -(pressure - parameters.referencePressure) * activationVolume / + thermalEnergy; + } + + static T stressFactor(T exponent) { + if (!std::isfinite(exponent)) + return T(1); + if (exponent <= std::log(minStressFactor)) + return minStressFactor; + if (exponent >= std::log(maxStressFactor)) + return maxStressFactor; + return std::exp(exponent); + } + + Vec3D computeSiNormal(const IndexType &index, + ConstSparseIterator &reactionIt) const { + // Reflect neighbor indices that fall outside the HRLE grid. This handles + // REFLECTIVE boundary conditions correctly: phi(b-k) = phi(b+k), so the + // centered difference at b gives zero lateral gradient as expected. + auto reflectToGrid = [&](IndexType idx) { + auto &g = reactionInterface->getGrid(); + for (unsigned d2 = 0; d2 < D; ++d2) { + const auto lo = g.getMinGridPoint(d2); + const auto hi = g.getMaxGridPoint(d2); + if (idx[d2] < lo) + idx[d2] = 2 * lo - idx[d2]; + if (idx[d2] > hi) + idx[d2] = 2 * hi - idx[d2]; + } + return idx; + }; + Vec3D gradient{0., 0., 0.}; + for (unsigned d = 0; d < D; ++d) { + IndexType plus = index, minus = index; + plus[d] += 1; + minus[d] -= 1; + gradient[d] = + (detail::clampLevelSetPhi(valueAt(reactionIt, reflectToGrid(plus))) - + detail::clampLevelSetPhi( + valueAt(reactionIt, reflectToGrid(minus)))) / + (T(2) * gridDelta); + } + T len = T(0); + for (unsigned d = 0; d < D; ++d) + len += gradient[d] * gradient[d]; + len = std::sqrt(len); + if (len > std::numeric_limits::epsilon()) { + for (unsigned d = 0; d < D; ++d) + gradient[d] /= len; + } else { + gradient = Vec3D{0., 0., 0.}; + gradient[D - 1] = T(1); + } + return gradient; + } + + std::pair classifyBoundary(ConstSparseIterator &reactionIt, + ConstSparseIterator &ambientIt, + ConstSparseIterator &maskIt, + const IndexType &inside, + const IndexType &outside) const { + const T reactionInside = valueAt(reactionIt, inside); + const T reactionOutside = valueAt(reactionIt, outside); + const T ambientInside = valueAt(ambientIt, inside); + const T ambientOutside = valueAt(ambientIt, outside); + const T maskInside = valueAtMask(maskIt, inside); + const T maskOutside = valueAtMask(maskIt, outside); + + const T reactionDistance = + crosses(reactionInside, reactionOutside) + ? crossingDistance(reactionInside, reactionOutside) + : std::numeric_limits::max(); + const T ambientDistance = + crosses(ambientInside, ambientOutside) + ? crossingDistance(ambientInside, ambientOutside) + : std::numeric_limits::max(); + const T maskDistance = + maskInterface != nullptr && crosses(maskInside, maskOutside) + ? crossingDistance(maskInside, maskOutside) + : std::numeric_limits::max(); + + if (reactionDistance == std::numeric_limits::max() && + ambientDistance == std::numeric_limits::max() && + maskDistance == std::numeric_limits::max()) + return {Boundary::NONE, gridDelta}; + + if (reactionDistance <= ambientDistance && reactionDistance <= maskDistance) + return {Boundary::REACTION, reactionDistance}; + + // Ambient crossing heads into mask-occupied space: the oxide/gas surface + // has drifted under the nitride. Classify as MASK regardless of whether + // the crossings are coincident (isMaskAtCrossing) or the outer node is + // wholly inside the mask body (valueAtMask check). This is equivalent to + // sealing the diffusion domain with UNION(ambientInterface, maskInterface) + // without mutating any level set. + if (ambientDistance != std::numeric_limits::max() && + (isMaskAtCrossing(maskInside, maskOutside, ambientDistance) || + (maskInterface != nullptr && + static_cast(maskSign) * valueAtMask(maskIt, outside) >= T(0)))) + return {Boundary::MASK, ambientDistance}; + + if (maskDistance <= ambientDistance) + return {Boundary::MASK, maskDistance}; + return {Boundary::AMBIENT, ambientDistance}; + } + + bool isInsideOxide(T reactionPhi, T ambientPhi) const { + // GeometricAdvect can leave a tiny positive residual (~4*epsilon) when the + // interface lands exactly on a grid point at non-zero coordinates, because + // k*gridDelta is not exactly representable in floating point. Allow a + // tolerance of 1e-9 grid units so that grid points on the surface (phiβ‰ˆ0) + // are correctly classified as inside the oxide. + constexpr T eps = T(1e-9); + return reactionSign * reactionPhi >= -eps && + ambientSign * ambientPhi >= -eps; + } + + ConstSparseIterator makeMaskIterator() const { + if (maskInterface == nullptr) + return ConstSparseIterator(reactionInterface->getDomain()); + return ConstSparseIterator(maskInterface->getDomain()); + } + + bool isInsideMask(ConstSparseIterator &maskIt, const IndexType &index) const { + if (maskInterface == nullptr) + return false; + return maskSign * valueAt(maskIt, index) >= 0.; + } + + T valueAtMask(ConstSparseIterator &maskIt, const IndexType &index) const { + if (maskInterface == nullptr) + return std::numeric_limits::max(); + return valueAt(maskIt, index); + } + + bool isMaskAtCrossing(T maskInside, T maskOutside, T distance) const { + if (maskInterface == nullptr) + return false; + const T fraction = std::clamp(distance / gridDelta, T(0), T(1)); + const T insidePhi = detail::clampLevelSetPhi(maskInside); + const T outsidePhi = detail::clampLevelSetPhi(maskOutside); + const T maskPhi = insidePhi + fraction * (outsidePhi - insidePhi); + return static_cast(maskSign) * maskPhi >= T(0); + } + + T crossingDistance(T insidePhi, T outsidePhi) const { + return detail::levelSetCrossingDistance( + insidePhi, outsidePhi, parameters.minBoundaryDistance, gridDelta); + } +}; + +} // namespace viennals diff --git a/include/viennals/lsOxidationMask.hpp b/include/viennals/lsOxidationMask.hpp new file mode 100644 index 00000000..4f4fb1b3 --- /dev/null +++ b/include/viennals/lsOxidationMask.hpp @@ -0,0 +1,2434 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace viennals { + +struct OxidationMaskParameters { + // Contact mode: + // 0 = kinematic: mask velocity at oxide-contact faces equals the solved + // oxide velocity (legacy Dirichlet BC). No traction computation. + // 1 = oneway: mask solved with oxide stress traction as Neumann BC + // (multigrid GMRES); oxide uses kinematic Dirichlet at mask faces. + // Stable; captures mask bending driven by oxide force. (Config + // aliases: "oneway", "traction", "1", "2".) + // 2 = elastic: multigrid-GMRES elastic solve with youngModulus and + // poissonRatio. Oxide contact prescribes elastic displacement + // (v_oxide Γ— dt). The outer coupling loop in lsOxidation feeds the + // solved mask displacement back into the next oxide solve. (Config + // aliases: "elastic", "twoway" and variants, "3", "4".) + int contactMode = 1; + // Arrhenius creep viscosity law: + // eta(T) = eta_ref * exp(E/R * (1/T - 1/T_ref)). + // Viscosity is in PaΒ·hr, activation energy in J/mol, temperature in K. + double temperature = 1273.15; + double referenceTemperature = 1273.15; + double referenceViscosity = 5e8; + double creepActivationEnergy = 0.; + // Young's modulus for elastic contact mode (2), in Pa. + // Si₃Nβ‚„ at 1000 Β°C: ~250–270 GPa. + double youngModulus = 270e9; + // Current solve timestep in hours; set by lsOxidation before each apply(). + // Elastic modes use it to scale oxide velocity to contact displacement + // (v * dt) and to convert the solved displacement back to advection velocity. + double stressTimeStep = 1; + double poissonRatio = 0.27; + // false = bonded mask/oxide interface (traction continuity in compression + // and tension); true = optional unilateral contact/release model. + bool unilateralContact = true; + // Outer Aitken relaxation factor applied to the mask/oxide coupling residual. + // Independent of the multigrid smoother below. + double relaxation = 1.; + // Under-relaxation for the unilateral contact load active set. A hard + // compressive/tensile switch makes the mask/oxide fixed point oscillate when + // contact faces release; relaxing the load keeps the complementarity limit + // but makes the iteration continuous. + double contactLoadRelaxation = 0.25; + // Relative pressure floor for releasing a relaxed contact face. Machine + // epsilon is far too small for Pa-scale contact stresses: a tensile face with + // a decaying old compressive load would otherwise remain "active" for many + // nonlinear iterations. The scale is the previous/current normal traction. + double contactReleaseFraction = 5e-3; + // SOR omega for the multigrid V-cycle smoother (both forward and backward + // sweeps). 1.0 is standard Gauss-Seidel; values in (1, 1.4] add over- + // relaxation. Do not share this with the Aitken relaxation above. + double multigridSmootherOmega = 1.0; + double tolerance = 1e-8; + // Minimum sub-grid boundary distance as a fraction of gridDelta. This is a + // mechanical derivative length scale, so keep it comparable to the oxide + // deformation solver; near-zero distances make elastic contact stresses blow + // up as E * u / d. + double minBoundaryDistance = 0.05; + unsigned maxIterations = 10000; + std::size_t maxGridPoints = 5000000; + int material = -1; + // Optional far-field clamp used to remove rigid-body mask drift and provide + // the support that makes mask thickness mechanically meaningful. A side of + // -1 clamps the lower index side, +1 clamps the upper side, and 0 disables + // the clamp. Direction defaults to x in a LOCOS cross-section. + int anchorBoundaryDirection = 0; + int anchorBoundarySide = -1; + unsigned anchorBoundaryLayers = 1; +}; + +/// Vector velocity field for a compliant oxidation mask driven by solved oxide +/// traction. A Cartesian viscous elasticity solve is built inside the mask +/// level set, oxide-contact faces use a traction ghost velocity, and the +/// resulting vector field moves the entire mask body. Repeated applications use +/// Aitken relaxation on the contact-interface velocity update. +template +class OxidationMaskBending final : public VelocityField, + public OxidationSolverBase { + using IndexType = viennahrle::Index; + using ConstSparseIterator = + viennahrle::ConstSparseIterator::DomainType>; + +private: + // bring base members into scope + using OxidationSolverBase::noNode; + using OxidationSolverBase::nodeLookupFlat; + using OxidationSolverBase::initNodeLookup; + using OxidationSolverBase::lookupNode; + using OxidationSolverBase::minIndex; + using OxidationSolverBase::maxIndex; + using OxidationSolverBase::extents; + using OxidationSolverBase::strides; + using OxidationSolverBase::gridDelta; + using OxidationSolverBase::crosses; + using OxidationSolverBase::valueAt; + using OxidationSolverBase::inBounds; + using OxidationSolverBase::linearIndex; + using OxidationSolverBase::increment; + using OxidationSolverBase::findNearbyNode; + using OxidationSolverBase::initializeGridFromMask; + + struct Node { + IndexType index; + Vec3D velocity{0., 0., 0.}; + bool contact = false; + bool fixed = false; + }; + + struct SparseMatrix { + std::size_t nodeCount = 0; + std::vector rowPtr; + std::vector colIndex; + std::vector values; + std::vector invDiagonal; + }; + + struct MultigridLevel { + std::vector indices; + // For level l > 0, children maps each coarse node to level l-1 nodes. + std::vector> children; + std::vector fineToCoarse; + SparseMatrix matrix; + }; + + SmartPointer> deformationField = nullptr; + SmartPointer> maskInterface = nullptr; + SmartPointer> ambientInterface = nullptr; + OxidationMaskParameters parameters; + int maskSign = 1; + int ambientSign = -1; + + bool solved = false; + bool useRequestedBounds = false; + T residual = std::numeric_limits::max(); + unsigned iterations = 0; + std::array maxVelocity_{}; + std::size_t contactNodes = 0; + std::size_t fixedNodes = 0; + T lastApplyVelocityChange = std::numeric_limits::max(); + T lastApplyAbsoluteVelocityChange = std::numeric_limits::max(); + T aitkenOmega = 1.; + std::size_t candidateContactFaces_ = 0; + std::size_t activeContactFaces_ = 0; + std::size_t tensileContactFaces_ = 0; + T minContactNormalTraction_ = std::numeric_limits::max(); + T maxContactNormalTraction_ = std::numeric_limits::lowest(); + T contactReleaseThreshold_ = 0.; + std::unordered_map> previousContactTraction_; + std::unordered_map previousContactReleaseScale_; + std::vector previousAitkenResidual; + // Elastic mode (contactMode==2): snapshot of u_new (elastic equilibrium + // displacement in Β΅m, stored as Β΅m/hr with implicit dt_ref=1 hr) taken by + // finalizeElasticAdvectionVelocity(). Used by writeFieldsToLevelSet() to + // write the "MaskVelocity" warm-start for the next substep's solver. + std::vector> elasticU_; + IndexType requestedMinIndex{}; + IndexType requestedMaxIndex{}; + std::vector nodes; + // Face-major flat contact BC arrays: index = faceIdx * n + nodeId. + std::vector contactFaceActive_; // 1 if contact BC applies + std::vector> contactFaceVelocity_; // Dirichlet velocity at contact + std::vector> contactFaceTraction_; // Oxide traction on mask face + std::vector contactFaceDistance_; // Node-to-interface distance + std::unordered_map ambientPhiCache_; + + // Cached multigrid hierarchy. The stiffness matrix depends on the node + // geometry and the contact face classification (active/inactive) but NOT on + // the traction magnitudes, which only affect the load vector b. When the + // contact pattern is unchanged from the previous apply() call, the hierarchy + // can be reused and the O(n) matrix build and Galerkin coarsening skipped. + std::vector cachedMultigridLevels_; + std::vector cachedContactFaceActive_; // snapshot at last build + std::size_t cachedNodeCount_ = 0; + +public: + OxidationMaskBending() = default; + + OxidationMaskBending( + SmartPointer> passedDeformation, + OxidationMaskParameters passedParameters = {}) + : deformationField(passedDeformation), parameters(passedParameters) {} + + OxidationMaskBending( + SmartPointer> passedDeformation, + SmartPointer> passedMaskInterface, + OxidationMaskParameters passedParameters = {}, int passedMaskSign = 1) + : deformationField(passedDeformation), maskInterface(passedMaskInterface), + parameters(passedParameters), maskSign((passedMaskSign < 0) ? -1 : 1) {} + + ~OxidationMaskBending() = default; + + static SmartPointer + New(SmartPointer> passedDeformation, + OxidationMaskParameters passedParameters = {}) { + return SmartPointer::New(passedDeformation, + passedParameters); + } + + static SmartPointer + New(SmartPointer> passedDeformation, + SmartPointer> passedMaskInterface, + OxidationMaskParameters passedParameters = {}, int passedMaskSign = 1) { + return SmartPointer::New( + passedDeformation, passedMaskInterface, passedParameters, + passedMaskSign); + } + + void setMaskInterface(SmartPointer> passedMaskInterface, + int passedMaskSign = 1) { + maskInterface = passedMaskInterface; + maskSign = (passedMaskSign < 0) ? -1 : 1; + solved = false; + } + + /// Provide the SiOβ‚‚/ambient interface so that contact faces can be detected + /// on any mask face that borders the oxide, not only the bottom face. + /// ambientSign follows the same convention as OxidationDeformation: + /// ambientSign * Ο†_ambient >= 0 selects nodes inside the oxide band. + void setAmbientInterface(SmartPointer> passedAmbientInterface, + int passedAmbientSign = -1) { + ambientInterface = passedAmbientInterface; + ambientSign = (passedAmbientSign < 0) ? -1 : 1; + solved = false; + } + + void setParameters(OxidationMaskParameters passedParameters) { + parameters = passedParameters; + solved = false; + } + + void setSolveBounds(const IndexType &passedMinIndex, + const IndexType &passedMaxIndex) { + requestedMinIndex = passedMinIndex; + requestedMaxIndex = passedMaxIndex; + useRequestedBounds = true; + solved = false; + } + + void clearSolveBounds() { + useRequestedBounds = false; + solved = false; + } + + OxidationMaskParameters getParameters() const { return parameters; } + unsigned getIterations() const { return iterations; } + T getResidual() const { return residual; } + std::size_t getNumberOfSolutionNodes() const { return nodes.size(); } + std::size_t getNumberOfContactNodes() const { return contactNodes; } + std::size_t getNumberOfFixedNodes() const { return fixedNodes; } + T getLastApplyVelocityChange() const { return lastApplyVelocityChange; } + T getLastApplyAbsoluteVelocityChange() const { + return lastApplyAbsoluteVelocityChange; + } + + void apply() { + if (maskInterface == nullptr) { + solved = true; + return; + } + if (deformationField == nullptr) { + Logger::getInstance() + .addWarning("OxidationMaskBending: deformation field is null; " + "mask bending will produce zero velocities.") + .print(); + solved = true; + return; + } + + const auto previousVelocities = collectVelocitiesByGridPoint(); + if (!initialiseGrid()) + return; // base class already logged the error + elasticU_ + .clear(); // reset before coupling so getVectorVelocity divides by dt + buildNodes(); + seedFromLevelSet(); // warm-start velocity from previous substep's pointData + seedFromPrevious(previousVelocities); + if (nodes.empty()) { + Logger::getInstance() + .addWarning("OxidationMaskBending: no mask nodes found after " + "buildNodes(). Verify that the mask level set defines " + "a non-empty interior region within the solve bounds.") + .print(); + solved = true; + return; + } + if (contactNodes == 0) + VIENNACORE_LOG_DEBUG( + "OxidationMaskBending: mask has no oxide-contact nodes; " + "no traction will be applied this step."); + solveElasticVelocity(); + validateNodeVelocities("solveElasticVelocity"); + smoothVelocityField(); + + const auto fixedPointResidual = contactResidualVector(previousVelocities); + const T omega = aitkenRelaxation(fixedPointResidual); + relaxVelocities(previousVelocities, omega); + validateNodeVelocities("Aitken relaxation"); + lastApplyVelocityChange = maxVelocityChange(previousVelocities); + lastApplyAbsoluteVelocityChange = + absoluteVelocityChange(previousVelocities); + + maxVelocity_.fill(T(0)); + for (const auto &node : nodes) { + for (unsigned d = 0; d < D; ++d) + maxVelocity_[d] = std::max(maxVelocity_[d], std::abs(node.velocity[d])); + } + VIENNACORE_LOG_DEBUG( + "OxidationMaskBending: contact faces active=" + + std::to_string(activeContactFaces_) + "/" + + std::to_string(candidateContactFaces_) + ", tensileSkipped=" + + std::to_string(tensileContactFaces_) + ", normalTraction=[" + + std::to_string( + candidateContactFaces_ == 0 ? T(0) : minContactNormalTraction_) + + ", " + + std::to_string( + candidateContactFaces_ == 0 ? T(0) : maxContactNormalTraction_) + + "], aitkenOmega=" + std::to_string(omega) + ", contactLoadRelaxation=" + + std::to_string( + std::clamp(parameters.contactLoadRelaxation, T(0.02), T(1))) + + ", contactReleaseThreshold=" + + std::to_string(contactReleaseThreshold_) + ", residual=" + + std::to_string(lastApplyVelocityChange) + ", absVelocityChange=" + + std::to_string(lastApplyAbsoluteVelocityChange)); + + solved = true; + } + + // Called from lsOxidation after writeFieldsToLevelSet() and before advect(). + // + // Elastic mode (2): the contact faces use kinematic (Dirichlet) BC so the + // mask stays bonded to the oxide surface. The solver output u_new is the + // elastic bending displacement (in Β΅m, stored as Β΅m/hr with implicit + // dt_ref=1 hr). Convert to advection velocity u_new/dt and update + // maxVelocity_ for CFL subcycling. No-op for viscous modes. + void finalizeElasticAdvectionVelocity() { + if (!isElasticContactMode() || nodes.empty()) + return; + const T dt = parameters.stressTimeStep; + if (dt <= T(0)) { + for (auto &node : nodes) + node.velocity = {T(0), T(0), T(0)}; + return; + } + + const std::size_t n = nodes.size(); + elasticU_.resize(n); + for (std::size_t i = 0; i < n; ++i) + elasticU_[i] = nodes[i].velocity; + + T maxUNew = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned d = 0; d < D; ++d) + maxUNew = std::max(maxUNew, std::abs(elasticU_[i][d])); + + const T invDt = T(1) / dt; + for (std::size_t i = 0; i < n; ++i) + for (unsigned d = 0; d < D; ++d) + nodes[i].velocity[d] = elasticU_[i][d] * invDt; + + VIENNACORE_LOG_INFO("ElasticFinalize: dt=" + std::to_string(dt) + + " max|u_new|=" + std::to_string(maxUNew) + + " max|u_new/dt|=" + std::to_string(maxUNew * invDt)); + + maxVelocity_.fill(T(0)); + for (const auto &node : nodes) + for (unsigned d = 0; d < D; ++d) + maxVelocity_[d] = std::max(maxVelocity_[d], std::abs(node.velocity[d])); + } + + Vec3D getVectorVelocity(const Vec3D &coordinate, int material, + const Vec3D & /*normalVector*/, + unsigned long /*pointId*/) final { + if (deformationField == nullptr) + return {0., 0., 0.}; + + if (parameters.material >= 0 && material != parameters.material) + return {0., 0., 0.}; + + if (!solved) + apply(); + + if (maskInterface == nullptr || nodes.empty()) + return {0., 0., 0.}; + + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / gridDelta); + + // Elastic mode: the solver stores u_new (displacement in Β΅m) as if it were + // a velocity (Β΅m/hr, with implicit dt_ref=1hr). The oxide needs the actual + // advection velocity u_new/dt. Before finalization elasticU_ is empty and + // nodes[i].velocity = u_new; we divide by dt here. After finalization + // nodes[i].velocity = u_new/dt already, so we return it directly. + if (isElasticContactMode() && elasticU_.empty()) { + const T dt = parameters.stressTimeStep; + if (dt <= T(0)) + return {T(0), T(0), T(0)}; + return detail::vecScaled(getVelocity(index), T(1) / dt); + } + + return getVelocity(index); + } + + T getDissipationAlpha(int direction, int /*material*/, + const Vec3D & /*centralDifferences*/) final { + return maxVelocity_[direction]; + } + + /// Write mask bending velocity into maskInterface->getPointData() so that + /// lsInterior + lsAdvect carry it across timestep boundaries. + void writeFieldsToLevelSet() { + if (nodes.empty() || maskInterface == nullptr) + return; + + using VD = typename PointData::VectorDataType; + + // Elastic mode: after finalization, elasticU_ holds u_new (the displacement + // in Β΅m, stored as Β΅m/hr). Use it as the "MaskVelocity" warm-start so the + // next substep's solver begins near its expected solution. + // Before finalization (elasticU_ empty), fall back to nodes[nId].velocity + // which still equals u_new from the current solve. + const bool useElasticU = isElasticContactMode() && !elasticU_.empty(); + + VD velocity; + ConstSparseIterator it(maskInterface->getDomain()); + for (; !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + const std::size_t nId = lookupNode(it.getStartIndices()); + Vec3D v{T(0), T(0), T(0)}; + if (nId != noNode) + v = (useElasticU && nId < elasticU_.size()) ? elasticU_[nId] + : nodes[nId].velocity; + velocity.push_back(v); + } + maskInterface->getPointData().insertReplaceVectorData(std::move(velocity), + "MaskVelocity"); + + // Cauchy stress tensor in the mask: Οƒ = Ξ»(βˆ‡Β·u)I + 2ΞΌΒ·sym(βˆ‡u) + // In elastic mode use the raw displacement u_new (elasticU_); in viscous + // mode use the velocity field β€” both give the correct stress via the same + // LamΓ© constants (which already encode the physical units). + const T lambda = lameLambda(); + const T mu = lameMu(); + + std::vector> velForStress(nodes.size()); + for (std::size_t i = 0; i < nodes.size(); ++i) + velForStress[i] = (useElasticU && i < elasticU_.size()) + ? elasticU_[i] + : nodes[i].velocity; + + VD stressR0, stressR1, stressR2; + for (ConstSparseIterator sit(maskInterface->getDomain()); !sit.isFinished(); + ++sit) { + if (!sit.isDefined()) + continue; + const std::size_t nId = lookupNode(sit.getStartIndices()); + Vec3D row0{T(0), T(0), T(0)}; + Vec3D row1{T(0), T(0), T(0)}; + Vec3D row2{T(0), T(0), T(0)}; + if (nId != noNode) { + // grad[j][i] = βˆ‚v_i/βˆ‚x_j via central differences + std::array, 3> grad{}; + for (unsigned j = 0; j < static_cast(D); ++j) { + const auto vp = simpleNeighborVelocity(velForStress, nId, j, +1); + const auto vm = simpleNeighborVelocity(velForStress, nId, j, -1); + for (unsigned i = 0; i < 3; ++i) + grad[j][i] = (vp[i] - vm[i]) / (T(2) * gridDelta); + } + T div = T(0); + for (unsigned i = 0; i < static_cast(D); ++i) + div += grad[i][i]; + // Οƒ_ij = λ·divΒ·Ξ΄_ij + ΞΌΒ·(βˆ‚v_i/βˆ‚x_j + βˆ‚v_j/βˆ‚x_i) + row0[0] = lambda * div + T(2) * mu * grad[0][0]; + row0[1] = mu * (grad[1][0] + grad[0][1]); + row0[2] = (D > 2) ? mu * (grad[2][0] + grad[0][2]) : T(0); + row1[0] = row0[1]; + row1[1] = lambda * div + T(2) * mu * grad[1][1]; + row1[2] = (D > 2) ? mu * (grad[2][1] + grad[1][2]) : T(0); + row2[0] = row0[2]; + row2[1] = row1[2]; + row2[2] = (D > 2) ? lambda * div + T(2) * mu * grad[2][2] : T(0); + } + stressR0.push_back(row0); + stressR1.push_back(row1); + stressR2.push_back(row2); + } + maskInterface->getPointData().insertReplaceVectorData(std::move(stressR0), + "MaskStressR0"); + maskInterface->getPointData().insertReplaceVectorData(std::move(stressR1), + "MaskStressR1"); + maskInterface->getPointData().insertReplaceVectorData(std::move(stressR2), + "MaskStressR2"); + } + +private: + bool initialiseGrid() { + return initializeGridFromMask( + maskInterface, useRequestedBounds, requestedMinIndex, requestedMaxIndex, + parameters.maxGridPoints, "OxidationMaskBending"); + } + + /// Seed nodes[i].velocity from maskInterface->getPointData()["MaskVelocity"] + /// so solveElasticVelocity() warm-starts from the previous substep's field. + void seedFromLevelSet() { + if (maskInterface == nullptr || nodes.empty()) + return; + const int vIdx = + maskInterface->getPointData().getVectorDataIndex("MaskVelocity"); + if (vIdx == -1) + return; + const auto *vd = maskInterface->getPointData().getVectorData(vIdx); + if (vd == nullptr) + return; + + ConstSparseIterator it(maskInterface->getDomain()); + for (; !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + const auto ptId = it.getPointId(); + if (ptId >= static_cast(vd->size())) + continue; + const std::size_t nId = lookupNode(it.getStartIndices()); + if (nId == noNode) + continue; + if (!isFinite((*vd)[ptId])) + throwNonFinite("stored mask velocity point data"); + else + nodes[nId].velocity = (*vd)[ptId]; + } + } + + void + seedFromPrevious(const std::unordered_map> &previous) { + if (previous.empty()) + return; + for (auto &node : nodes) { + const auto found = previous.find(linearIndex(node.index)); + if (found != previous.end() && isFinite(found->second)) + node.velocity = found->second; + if (node.fixed) + node.velocity = {T(0), T(0), T(0)}; + } + } + + std::unordered_map> + collectVelocitiesByGridPoint() const { + std::unordered_map> result; + result.reserve(nodes.size()); + for (const auto &node : nodes) + if (inBounds(node.index) && isFinite(node.velocity)) + result.emplace(linearIndex(node.index), node.velocity); + return result; + } + + static bool isFinite(const Vec3D &v) { + for (unsigned i = 0; i < 3; ++i) + if (!std::isfinite(v[i])) + return false; + return true; + } + + static bool isFiniteTensor(const std::array &tensor) { + for (T value : tensor) + if (!std::isfinite(value)) + return false; + return true; + } + + [[noreturn]] void throwNonFinite(const std::string &stage) const { + const std::string message = + "OxidationMaskBending: " + stage + " produced non-finite values."; + Logger::getInstance().addError(message).print(); + throw std::runtime_error(message); + } + + void validateNodeVelocities(const std::string &stage) const { + for (const auto &node : nodes) + for (unsigned i = 0; i < D; ++i) + if (!std::isfinite(node.velocity[i])) + throwNonFinite(stage); + } + + T maxVelocityChange( + const std::unordered_map> &previous) const { + if (previous.empty()) + return std::numeric_limits::max(); + + T changeSquaredSum = 0.; + T magnitudeSquaredSum = std::numeric_limits::epsilon(); + for (const auto &node : nodes) { + if (!node.contact) + continue; + const auto found = previous.find(linearIndex(node.index)); + if (found == previous.end()) + continue; + + for (unsigned i = 0; i < D; ++i) { + const T delta = node.velocity[i] - found->second[i]; + changeSquaredSum += delta * delta; + magnitudeSquaredSum += node.velocity[i] * node.velocity[i]; + magnitudeSquaredSum += found->second[i] * found->second[i]; + } + } + + const T change = std::sqrt(changeSquaredSum / magnitudeSquaredSum); + if (!std::isfinite(change)) + throwNonFinite("mask velocity coupling residual"); + return change; + } + + T absoluteVelocityChange( + const std::unordered_map> &previous) const { + if (previous.empty()) + return std::numeric_limits::max(); + + T changeSquaredSum = 0.; + std::size_t components = 0; + for (const auto &node : nodes) { + if (!node.contact) + continue; + const auto found = previous.find(linearIndex(node.index)); + if (found == previous.end()) + continue; + + for (unsigned i = 0; i < D; ++i) { + const T delta = node.velocity[i] - found->second[i]; + if (!std::isfinite(delta)) + throwNonFinite("mask absolute velocity coupling residual"); + changeSquaredSum += delta * delta; + ++components; + } + } + + if (components == 0) + return std::numeric_limits::max(); + const T change = std::sqrt(changeSquaredSum / static_cast(components)); + if (!std::isfinite(change)) + throwNonFinite("mask absolute velocity coupling residual"); + return change; + } + + std::vector contactResidualVector( + const std::unordered_map> &previous) const { + std::vector residualVector; + residualVector.reserve(contactNodes * D); + if (previous.empty()) + return residualVector; + + for (const auto &node : nodes) { + if (!node.contact) + continue; + const auto found = previous.find(linearIndex(node.index)); + if (found == previous.end()) + continue; + for (unsigned i = 0; i < D; ++i) { + const T residualComponent = node.velocity[i] - found->second[i]; + if (!std::isfinite(residualComponent)) + throwNonFinite("mask contact residual"); + residualVector.push_back(residualComponent); + } + } + return residualVector; + } + + T aitkenRelaxation(const std::vector &residualVector) { + const T baseOmega = std::clamp(parameters.relaxation, T(0.01), T(1)); + if (residualVector.empty()) { + previousAitkenResidual.clear(); + aitkenOmega = baseOmega; + return 1.; + } + + T omega = std::clamp(aitkenOmega, T(0.01), baseOmega); + if (previousAitkenResidual.size() != residualVector.size()) + omega = baseOmega; + if (previousAitkenResidual.size() == residualVector.size()) { + T numerator = 0.; + T denominator = 0.; + T residualNorm2 = 0.; + T previousNorm2 = 0.; + for (std::size_t i = 0; i < residualVector.size(); ++i) { + if (!std::isfinite(residualVector[i]) || + !std::isfinite(previousAitkenResidual[i])) + throwNonFinite("mask Aitken residual"); + const T delta = residualVector[i] - previousAitkenResidual[i]; + numerator += previousAitkenResidual[i] * delta; + denominator += delta * delta; + residualNorm2 += residualVector[i] * residualVector[i]; + previousNorm2 += previousAitkenResidual[i] * previousAitkenResidual[i]; + } + + if (!std::isfinite(numerator) || !std::isfinite(denominator)) + throwNonFinite("mask Aitken coefficient"); + + if (denominator > std::numeric_limits::epsilon()) { + omega = -aitkenOmega * numerator / denominator; + if (std::isfinite(omega)) { + // Traction contact mode: cap at 1.0 (no extrapolation). The + // compressive/tensile contact state can alternate across coupling + // iterations, and omega > 1 extrapolates through the fixed point + // for those nodes, producing sign-alternating velocities that + // appear as zigzag kinks after level-set advection. + const T omegaMax = (parameters.contactMode > 0) ? baseOmega : T(1.5); + omega = std::clamp(omega, T(0.05), omegaMax); + } else { + throwNonFinite("mask Aitken coefficient"); + } + } + + if (std::isfinite(residualNorm2) && std::isfinite(previousNorm2) && + residualNorm2 > previousNorm2 * T(1.05)) { + omega = std::min(omega, std::max(T(0.05), aitkenOmega * T(0.5))); + } + } + + previousAitkenResidual = residualVector; + aitkenOmega = omega; + return omega; + } + + // One pass of Laplacian (neighbour-average) smoothing on the mask velocity + // field. Damps grid-scale oscillations that arise near the contact-to-free + // boundary transition without materially changing the bulk velocity. + // Fixed (anchor) nodes are skipped β€” they must stay at zero. + void smoothVelocityField() { + const std::size_t n = nodes.size(); + if (n == 0) + return; + + std::vector> smoothed(n); + for (std::size_t id = 0; id < n; ++id) { + if (nodes[id].fixed) { + smoothed[id] = {T(0), T(0), T(0)}; + continue; + } + Vec3D sum = nodes[id].velocity; + int count = 1; + for (unsigned dir = 0; dir < D; ++dir) { + for (int off : {-1, 1}) { + IndexType nb = nodes[id].index; + nb[dir] += off; + if (!inBounds(nb)) + continue; + const std::size_t nbId = nodeLookupFlat[linearIndex(nb)]; + if (nbId == noNode) + continue; + for (unsigned c = 0; c < D; ++c) + sum[c] += nodes[nbId].velocity[c]; + ++count; + } + } + const T w = T(1) / static_cast(count); + for (unsigned c = 0; c < D; ++c) + smoothed[id][c] = sum[c] * w; + } + + for (std::size_t id = 0; id < n; ++id) + nodes[id].velocity = smoothed[id]; + } + + void + relaxVelocities(const std::unordered_map> &previous, + T omega) { + if (!std::isfinite(omega)) + throwNonFinite("mask Aitken coefficient"); + if (previous.empty() || omega == T(1)) + return; + + for (auto &node : nodes) { + const auto found = previous.find(linearIndex(node.index)); + if (found == previous.end()) + continue; + for (unsigned i = 0; i < D; ++i) { + const T relaxed = + found->second[i] + omega * (node.velocity[i] - found->second[i]); + if (!std::isfinite(relaxed)) + throwNonFinite("mask velocity relaxation"); + node.velocity[i] = relaxed; + } + } + } + + void buildAmbientPhiCache() { + ambientPhiCache_.clear(); + if (ambientInterface == nullptr) + return; + for (ConstSparseIterator it(ambientInterface->getDomain()); + !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + ambientPhiCache_[detail::gridIndexHash(it.getStartIndices())] = + static_cast(ambientSign) * it.getValue(); + } + } + + void buildNodes() { + auto oldContactTraction = std::move(previousContactTraction_); + auto oldContactReleaseScale = std::move(previousContactReleaseScale_); + previousContactTraction_.clear(); + previousContactReleaseScale_.clear(); + contactReleaseThreshold_ = T(0); + nodes.clear(); + initNodeLookup(); + buildAmbientPhiCache(); + + ConstSparseIterator maskIt(maskInterface->getDomain()); + IndexType index = minIndex; + while (true) { + if (isInsideMask(maskIt, index)) { + const std::size_t id = nodes.size(); + nodeLookupFlat[linearIndex(index)] = id; + nodes.push_back({index}); + } + + if (!increment(index)) + break; + } + + const std::size_t n = nodes.size(); + contactFaceActive_.assign(2 * D * n, uint8_t(0)); + contactFaceVelocity_.assign(2 * D * n, Vec3D{T(0), T(0), T(0)}); + contactFaceTraction_.assign(2 * D * n, Vec3D{T(0), T(0), T(0)}); + contactFaceDistance_.assign(2 * D * n, gridDelta); + + contactNodes = 0; + candidateContactFaces_ = 0; + activeContactFaces_ = 0; + tensileContactFaces_ = 0; + minContactNormalTraction_ = std::numeric_limits::max(); + maxContactNormalTraction_ = std::numeric_limits::lowest(); + for (std::size_t id = 0; id < n; ++id) { + auto &node = nodes[id]; + node.contact = touchesContactBoundary(maskIt, node.index); + if (node.contact) + ++contactNodes; + + for (unsigned dir = 0; dir < D; ++dir) { + for (int offset : {-1, 1}) { + const unsigned faceIdx = dir * 2u + (offset == 1 ? 1u : 0u); + IndexType neighbor = node.index; + neighbor[dir] += offset; + const bool neighborIsNode = + inBounds(neighbor) && lookupNode(neighbor) != noNode; + if (neighborIsNode || + !isContactBoundary(node.index, dir, offset, maskIt)) + continue; // inactive already set by assign() + + const T faceDistance = maskFaceDistance(maskIt, node.index, neighbor); + ++candidateContactFaces_; + Vec3D faceNormal{0., 0., 0.}; + faceNormal[dir] = static_cast(offset); + Vec3D oxidePt{0., 0., 0.}; + for (unsigned i = 0; i < D; ++i) + oxidePt[i] = node.index[i] * gridDelta; + oxidePt[dir] += offset * gridDelta; + + const auto sigma = deformationField->getStressTensor(oxidePt); + if (!isFiniteTensor(sigma)) + throwNonFinite("oxide stress at mask contact"); + Vec3D t{0., 0., 0.}; + for (unsigned i = 0; i < D; ++i) + for (unsigned j = 0; j < D; ++j) + t[i] += sigma[3 * i + j] * faceNormal[j]; + + T tn = 0.; + for (unsigned i = 0; i < D; ++i) + tn += t[i] * faceNormal[i]; + + if (!isFinite(t) || !std::isfinite(tn)) + throwNonFinite("oxide traction at mask contact"); + minContactNormalTraction_ = std::min(minContactNormalTraction_, tn); + maxContactNormalTraction_ = std::max(maxContactNormalTraction_, tn); + + Vec3D contactLoad = t; + if (parameters.unilateralContact && tn >= T(0)) { + ++tensileContactFaces_; + contactLoad = {T(0), T(0), T(0)}; + } + + if (parameters.contactMode > 0) { + const auto key = contactFaceKey(node.index, dir, offset); + const auto prevIt = oldContactTraction.find(key); + if (prevIt != oldContactTraction.end()) { + const T alpha = + std::clamp(parameters.contactLoadRelaxation, T(0.02), T(1)); + for (unsigned c = 0; c < D; ++c) + contactLoad[c] = prevIt->second[c] + + alpha * (contactLoad[c] - prevIt->second[c]); + } + } + + T loadNormal = T(0); + for (unsigned i = 0; i < D; ++i) + loadNormal += contactLoad[i] * faceNormal[i]; + + T releaseThreshold = std::numeric_limits::epsilon(); + T releaseScale = std::max(std::abs(tn), T(1)); + if (parameters.contactMode > 0 && parameters.unilateralContact) { + const auto scaleIt = oldContactReleaseScale.find( + contactFaceKey(node.index, dir, offset)); + if (scaleIt != oldContactReleaseScale.end()) + releaseScale = std::max(releaseScale, scaleIt->second); + releaseThreshold = + std::clamp(parameters.contactReleaseFraction, T(0), T(0.25)) * + releaseScale; + contactReleaseThreshold_ = + std::max(contactReleaseThreshold_, releaseThreshold); + } + + if (parameters.unilateralContact && loadNormal >= -releaseThreshold) + continue; + + if (!isFinite(contactLoad)) + throwNonFinite("relaxed oxide contact load"); + + if (parameters.contactMode > 0) { + const auto key = contactFaceKey(node.index, dir, offset); + previousContactTraction_[key] = contactLoad; + if (parameters.unilateralContact) + previousContactReleaseScale_[key] = + std::max(releaseScale, std::abs(loadNormal)); + } + + contactFaceActive_[faceIdx * n + id] = uint8_t(1); + ++activeContactFaces_; + contactFaceTraction_[faceIdx * n + id] = contactLoad; + contactFaceDistance_[faceIdx * n + id] = faceDistance; + if (usesKinematicContactBoundary()) { + auto oxVel = deformationField->getVectorVelocity( + oxidePt, parameters.material, faceNormal, 0); + if (isElasticContactMode()) { + // Scale by dt: elastic solver uses dt_ref=1hr so the Dirichlet BC + // must be the physical displacement (v_oxide Γ— dt), not the + // velocity. + oxVel = detail::vecScaled(oxVel, parameters.stressTimeStep); + } + contactFaceVelocity_[faceIdx * n + id] = oxVel; + } + } + } + } + markFixedNodes(); + } + + std::size_t contactFaceKey(const IndexType &index, unsigned direction, + int offset) const { + std::size_t seed = detail::gridIndexHash(index); + seed ^= std::hash{}(direction) + + std::size_t(0x9e3779b97f4a7c15ULL) + (seed << 6) + (seed >> 2); + seed ^= std::hash{}(offset) + std::size_t(0x9e3779b97f4a7c15ULL) + + (seed << 6) + (seed >> 2); + return seed; + } + + // Returns neighbor velocity without ghost extrapolation. + // All arithmetic is in T; reads from the SolverT work vector are widened. + template + Vec3D simpleNeighborVelocity(const std::vector> &velocity, + std::size_t nodeId, unsigned direction, + int offset) const { + IndexType neighbor = nodes[nodeId].index; + neighbor[direction] += offset; + const auto toT = [](const Vec3D &v) -> Vec3D { + return {static_cast(v[0]), static_cast(v[1]), static_cast(v[2])}; + }; + if (!inBounds(neighbor)) + return toT(velocity[nodeId]); + const std::size_t foundId = nodeLookupFlat[linearIndex(neighbor)]; + return (foundId != noNode) ? toT(velocity[foundId]) : toT(velocity[nodeId]); + } + + // Ghost velocity enforcing sigma_mask * n = traction at a mask boundary face. + template + Vec3D stressBoundaryGhost(const std::vector> &velocity, + std::size_t nodeId, unsigned normalDir, + int offset, T distance, + const Vec3D &traction) const { + const T lambda = lameLambda(); + const T mu = lameMu(); + const T denom = + std::max(lambda + T(2) * mu, std::numeric_limits::epsilon()); + const T signedOffset = static_cast(offset); + Vec3D ghost{static_cast(velocity[nodeId][0]), + static_cast(velocity[nodeId][1]), + static_cast(velocity[nodeId][2])}; + T tangentialDivergence = T(0); + std::array normalTangentialDerivative{}; + for (unsigned tanDir = 0; tanDir < D; ++tanDir) { + if (tanDir == normalDir) + continue; + const Vec3D vPlus = + simpleNeighborVelocity(velocity, nodeId, tanDir, +1); + const Vec3D vMinus = + simpleNeighborVelocity(velocity, nodeId, tanDir, -1); + tangentialDivergence += + (vPlus[tanDir] - vMinus[tanDir]) / (T(2) * gridDelta); + normalTangentialDerivative[tanDir] = + (vPlus[normalDir] - vMinus[normalDir]) / (T(2) * gridDelta); + } + + const T normalTraction = traction[normalDir] * signedOffset; + const T normalDerivative = + (normalTraction - lambda * tangentialDivergence) / denom; + ghost[normalDir] += signedOffset * distance * normalDerivative; + + for (unsigned tanDir = 0; tanDir < D; ++tanDir) { + if (tanDir == normalDir) + continue; + const T normalDerivativeTangential = + signedOffset * traction[tanDir] / + std::max(mu, std::numeric_limits::epsilon()) - + normalTangentialDerivative[tanDir]; + ghost[tanDir] += signedOffset * distance * normalDerivativeTangential; + } + return ghost; + } + + template + Vec3D tractionFreeGhost(const std::vector> &velocity, + std::size_t nodeId, unsigned normalDir, + int offset) const { + return stressBoundaryGhost(velocity, nodeId, normalDir, offset, gridDelta, + Vec3D{T(0), T(0), T(0)}); + } + + template + Vec3D neighborVelocity(const std::vector> &velocity, + std::size_t nodeId, unsigned direction, + int offset) const { + IndexType neighbor = nodes[nodeId].index; + neighbor[direction] += offset; + + if (inBounds(neighbor)) { + const std::size_t foundId = nodeLookupFlat[linearIndex(neighbor)]; + if (foundId != noNode) + return {static_cast(velocity[foundId][0]), + static_cast(velocity[foundId][1]), + static_cast(velocity[foundId][2])}; + } + + const unsigned faceIdx = direction * 2u + (offset == 1 ? 1u : 0u); + const std::size_t nn = nodes.size(); + if (contactFaceActive_[faceIdx * nn + nodeId]) { + if (usesKinematicContactBoundary()) + return detail::vecSubtract( + detail::vecScaled(contactFaceVelocity_[faceIdx * nn + nodeId], + T(2)), + Vec3D{static_cast(velocity[nodeId][0]), + static_cast(velocity[nodeId][1]), + static_cast(velocity[nodeId][2])}); + + return stressBoundaryGhost(velocity, nodeId, direction, offset, + contactFaceDistance_[faceIdx * nn + nodeId], + contactFaceTraction_[faceIdx * nn + nodeId]); + } + + return tractionFreeGhost(velocity, nodeId, direction, offset); + } + + template + Vec3D divergenceGradient(const std::vector> &velocity, + const IndexType &index) const { + Vec3D gradient{T(0), T(0), T(0)}; + for (unsigned component = 0; component < D; ++component) { + IndexType plus = index; + IndexType minus = index; + plus[component] += 1; + minus[component] -= 1; + const T plusDiv = divergence(velocity, plus); + const T minusDiv = divergence(velocity, minus); + gradient[component] = (plusDiv - minusDiv) / (T(2) * gridDelta); + } + return gradient; + } + + template + T divergence(const std::vector> &velocity, + const IndexType &index) const { + if (!inBounds(index)) + return T(0); + const std::size_t nodeId = nodeLookupFlat[linearIndex(index)]; + if (nodeId == noNode) + return T(0); + + T result = T(0); + for (unsigned direction = 0; direction < D; ++direction) { + const Vec3D plus = neighborVelocity(velocity, nodeId, direction, 1); + const Vec3D minus = neighborVelocity(velocity, nodeId, direction, -1); + result += (plus[direction] - minus[direction]) / (T(2) * gridDelta); + } + return result; + } + + // Evaluates the elastic stencil F(v)[i] = laplaceAverage + gradDivCorrection. + // (AΒ·v)[i] = v[i] - F(v)[i] + b[i], where b[i] = F(0)[i] (contact BC + // constants). + template + Vec3D computeElasticStencilAt(std::size_t nodeId, + const std::vector> &v, + T gradDivWeight) const { + Vec3D laplaceAverage{T(0), T(0), T(0)}; + for (unsigned direction = 0; direction < D; ++direction) + for (int offset : {-1, 1}) + detail::vecAddTo(laplaceAverage, + neighborVelocity(v, nodeId, direction, offset)); + + const T count = static_cast(2 * D); + const Vec3D lapAvg = detail::vecScaled(laplaceAverage, T(1) / count); + const Vec3D gradDivCorr = detail::vecScaled( + divergenceGradient(v, nodes[nodeId].index), + gradDivWeight * gridDelta * gridDelta / (T(2) * static_cast(D))); + + return detail::vecAdd(lapAvg, gradDivCorr); + } + + // (Av)[i] = v[i] - F(v)[i] + b[i], stored as SolverT. + template + void elasticMatvec(const std::vector> &v, + const std::vector> &b, T gradDivWeight, + std::vector> &Av) const { +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < nodes.size(); ++i) { + if (nodes[i].fixed) { + for (unsigned c = 0; c < D; ++c) + Av[i][c] = v[i][c]; + continue; + } + const Vec3D Fv = computeElasticStencilAt(i, v, gradDivWeight); + for (unsigned c = 0; c < D; ++c) + Av[i][c] = + static_cast(static_cast(v[i][c]) - Fv[c] + b[i][c]); + } + } + + static constexpr std::size_t mgNoNode = + std::numeric_limits::max(); + + static Vec3D zeroVec() { return Vec3D{T(0), T(0), T(0)}; } + + static IndexType coarsenIndex(const IndexType &index) { + IndexType coarse{}; + for (unsigned d = 0; d < D; ++d) { + const auto value = index[d]; + coarse[d] = (value >= 0) ? value / 2 : -((-value + 1) / 2); + } + return coarse; + } + + std::vector buildMultigridHierarchy() const { + std::vector levels; + levels.emplace_back(); + auto &fine = levels.back(); + fine.indices.reserve(nodes.size()); + for (const auto &node : nodes) + fine.indices.push_back(node.index); + + constexpr unsigned maxLevels = 12; + constexpr std::size_t minCoarsenNodes = 48; + while (levels.size() < maxLevels && + levels.back().indices.size() > minCoarsenNodes) { + const auto &previous = levels.back(); + MultigridLevel coarse; + coarse.indices.reserve(previous.indices.size() / 2 + 1); + coarse.children.reserve(previous.indices.size() / 2 + 1); + coarse.fineToCoarse.assign(previous.indices.size(), mgNoNode); + + std::unordered_map coarseLookup; + coarseLookup.reserve(previous.indices.size()); + for (std::size_t fineId = 0; fineId < previous.indices.size(); ++fineId) { + const IndexType coarseIndex = coarsenIndex(previous.indices[fineId]); + const std::size_t key = detail::gridIndexHash(coarseIndex); + auto found = coarseLookup.find(key); + if (found == coarseLookup.end()) { + const std::size_t coarseId = coarse.indices.size(); + coarseLookup.emplace(key, coarseId); + coarse.indices.push_back(coarseIndex); + coarse.children.emplace_back(); + found = coarseLookup.find(key); + } + + const std::size_t coarseId = found->second; + coarse.children[coarseId].push_back(fineId); + coarse.fineToCoarse[fineId] = coarseId; + } + + if (coarse.indices.empty() || + coarse.indices.size() >= previous.indices.size()) + break; + + // Stop if any grid dimension collapses to a single cell on the coarse + // level. For thin masks this happens quickly in the thickness direction, + // and a 1-cell-thick level loses all stress-gradient information across + // that dimension, making the smoother degenerate. + { + IndexType lo = coarse.indices.front(), hi = coarse.indices.front(); + for (const auto &idx : coarse.indices) + for (unsigned d = 0; d < D; ++d) { + lo[d] = std::min(lo[d], idx[d]); + hi[d] = std::max(hi[d], idx[d]); + } + bool degenerate = false; + for (unsigned d = 0; d < D; ++d) + if (hi[d] == lo[d]) + degenerate = true; + if (degenerate) + break; + } + + levels.push_back(std::move(coarse)); + } + return levels; + } + + static T vectorDot(const std::vector> &a, + const std::vector> &b) { + T sum = T(0); + for (std::size_t i = 0; i < a.size(); ++i) + for (unsigned c = 0; c < D; ++c) + sum += a[i][c] * b[i][c]; + return sum; + } + + static T vectorNorm(const std::vector> &v) { + return std::sqrt(std::max(vectorDot(v, v), T(0))); + } + + static void vectorScale(std::vector> &v, T scale) { + for (auto &entry : v) + for (unsigned c = 0; c < D; ++c) + entry[c] *= scale; + } + + static void vectorAxpy(std::vector> &y, T alpha, + const std::vector> &x) { + for (std::size_t i = 0; i < y.size(); ++i) + for (unsigned c = 0; c < D; ++c) + y[i][c] += alpha * x[i][c]; + } + + static void vectorSubtractInPlace(std::vector> &y, T alpha, + const std::vector> &x) { + for (std::size_t i = 0; i < y.size(); ++i) + for (unsigned c = 0; c < D; ++c) + y[i][c] -= alpha * x[i][c]; + } + + static T flatValue(const std::vector> &v, std::size_t row) { + return v[row / D][row % D]; + } + + static void flatSet(std::vector> &v, std::size_t row, T value) { + v[row / D][row % D] = value; + } + + void collectLocalCandidatesRecursive(unsigned dim, const IndexType ¢er, + IndexType &candidate, + std::vector &out) const { + if (dim == D) { + if (!inBounds(candidate)) + return; + const std::size_t nodeId = lookupNode(candidate); + if (nodeId != noNode) + out.push_back(nodeId); + return; + } + + for (int offset = -2; offset <= 2; ++offset) { + candidate[dim] = center[dim] + offset; + collectLocalCandidatesRecursive(dim + 1, center, candidate, out); + } + } + + void collectLocalCandidates(std::size_t rowNode, + std::vector &out) const { + out.clear(); + IndexType candidate = nodes[rowNode].index; + collectLocalCandidatesRecursive(0, nodes[rowNode].index, candidate, out); + out.push_back(rowNode); + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + } + + SparseMatrix + compressSparseRows(std::vector> &rows, + std::size_t nodeCount) const { + SparseMatrix matrix; + matrix.nodeCount = nodeCount; + matrix.rowPtr.assign(rows.size() + 1, 0); + matrix.invDiagonal.assign(rows.size(), T(1)); + + for (std::size_t row = 0; row < rows.size(); ++row) { + std::vector> entries(rows[row].begin(), + rows[row].end()); + std::sort(entries.begin(), entries.end(), + [](const auto &a, const auto &b) { return a.first < b.first; }); + + matrix.rowPtr[row] = matrix.values.size(); + T diagonal = T(0); + for (const auto &[col, value] : entries) { + if (std::abs(value) <= std::numeric_limits::epsilon() * T(100)) + continue; + if (!std::isfinite(value)) + throwNonFinite("traction sparse matrix assembly"); + matrix.colIndex.push_back(col); + matrix.values.push_back(value); + if (col == row) + diagonal += value; + } + + if (std::abs(diagonal) > std::numeric_limits::epsilon()) + matrix.invDiagonal[row] = T(1) / diagonal; + else + matrix.invDiagonal[row] = T(1); + } + matrix.rowPtr[rows.size()] = matrix.values.size(); + return matrix; + } + + SparseMatrix buildFineElasticMatrix(const std::vector> &b, + T gradDivWeight) const { + const std::size_t n = nodes.size(); + std::vector> rows(n * D); + std::vector> basis(n, zeroVec()); + std::vector candidates; + + for (std::size_t rowNode = 0; rowNode < n; ++rowNode) { + if (nodes[rowNode].fixed) { + for (unsigned rowComponent = 0; rowComponent < D; ++rowComponent) { + const std::size_t row = rowNode * D + rowComponent; + rows[row][row] = T(1); + } + continue; + } + + // Build A column-by-column via probing. The operator is (AΒ·v)[i][c] = + // v[i][c] - F(v)[i][c], where F is affine: F(v) = F_linear(v) + F(0). + // b[rowNode] = F(0)[rowNode] (the traction load vector). The matrix + // entry is the linear part only: + // A[row, col] = Ξ΄(row,col) - (F(e_col)[rowComp] - F(0)[rowComp]) + // = Ξ΄(row,col) - (Fbasis[rowComp] - b[rowNode][rowComp]) + // b is constant with respect to col so it cancels between probes; it + // is included here to isolate the linear part of F from its affine + // offset. + collectLocalCandidates(rowNode, candidates); + for (std::size_t colNode : candidates) { + for (unsigned colComponent = 0; colComponent < D; ++colComponent) { + basis[colNode][colComponent] = T(1); + const Vec3D Fbasis = + computeElasticStencilAt(rowNode, basis, gradDivWeight); + basis[colNode][colComponent] = T(0); + + for (unsigned rowComponent = 0; rowComponent < D; ++rowComponent) { + const std::size_t row = rowNode * D + rowComponent; + const std::size_t col = colNode * D + colComponent; + const T identity = + (rowNode == colNode && rowComponent == colComponent) ? T(1) + : T(0); + const T value = + identity - (Fbasis[rowComponent] - b[rowNode][rowComponent]); + rows[row][col] += value; + } + } + } + } + + return compressSparseRows(rows, n); + } + + SparseMatrix buildGalerkinMatrix(const SparseMatrix &fine, + const MultigridLevel &coarseLevel) const { + const std::size_t coarseNodes = coarseLevel.indices.size(); + std::vector> rows(coarseNodes * D); + + for (std::size_t fineRow = 0; fineRow < fine.nodeCount * D; ++fineRow) { + const std::size_t fineRowNode = fineRow / D; + if (fineRowNode >= coarseLevel.fineToCoarse.size()) + continue; + const std::size_t coarseRowNode = coarseLevel.fineToCoarse[fineRowNode]; + if (coarseRowNode == mgNoNode) + continue; + + const T restrictionWeight = + T(1) / static_cast(std::max( + coarseLevel.children[coarseRowNode].size(), 1)); + const std::size_t coarseRow = coarseRowNode * D + fineRow % D; + + for (std::size_t nz = fine.rowPtr[fineRow]; nz < fine.rowPtr[fineRow + 1]; + ++nz) { + const std::size_t fineCol = fine.colIndex[nz]; + const std::size_t fineColNode = fineCol / D; + if (fineColNode >= coarseLevel.fineToCoarse.size()) + continue; + const std::size_t coarseColNode = coarseLevel.fineToCoarse[fineColNode]; + if (coarseColNode == mgNoNode) + continue; + const std::size_t coarseCol = coarseColNode * D + fineCol % D; + rows[coarseRow][coarseCol] += restrictionWeight * fine.values[nz]; + } + } + + return compressSparseRows(rows, coarseNodes); + } + + void sparseMatvec(const SparseMatrix &matrix, const std::vector> &x, + std::vector> &Ax) const { + Ax.assign(matrix.nodeCount, zeroVec()); +#pragma omp parallel for schedule(static) + for (std::size_t row = 0; row < matrix.nodeCount * D; ++row) { + T sum = T(0); + for (std::size_t nz = matrix.rowPtr[row]; nz < matrix.rowPtr[row + 1]; + ++nz) + sum += matrix.values[nz] * flatValue(x, matrix.colIndex[nz]); + flatSet(Ax, row, sum); + } + } + + void multigridProlong(const std::vector &levels, + std::size_t coarseLevelId, + const std::vector> &coarse, + std::vector> &fine) const { + const auto &fineLevel = levels[coarseLevelId - 1]; + const auto &coarseLevel = levels[coarseLevelId]; + fine.assign(fineLevel.indices.size(), zeroVec()); + for (std::size_t coarseId = 0; coarseId < coarseLevel.children.size(); + ++coarseId) + for (std::size_t fineId : coarseLevel.children[coarseId]) + for (unsigned c = 0; c < D; ++c) + fine[fineId][c] = coarse[coarseId][c]; + } + + void multigridSmooth(const SparseMatrix &matrix, + const std::vector> &rhs, + std::vector> &x, unsigned sweeps, + T omega) const { + const std::size_t rows = matrix.nodeCount * D; + auto relaxRow = [&](std::size_t row) { + T offDiagonal = T(0); + for (std::size_t nz = matrix.rowPtr[row]; nz < matrix.rowPtr[row + 1]; + ++nz) { + const std::size_t col = matrix.colIndex[nz]; + if (col == row) + continue; + offDiagonal += matrix.values[nz] * flatValue(x, col); + } + const T updated = + (flatValue(rhs, row) - offDiagonal) * matrix.invDiagonal[row]; + flatSet(x, row, + flatValue(x, row) + omega * (updated - flatValue(x, row))); + }; + + for (unsigned sweep = 0; sweep < sweeps; ++sweep) { + for (std::size_t row = 0; row < rows; ++row) + relaxRow(row); + for (std::size_t row = rows; row-- > 0;) + relaxRow(row); + } + } + + void multigridResidual(const SparseMatrix &matrix, + const std::vector> &rhs, + const std::vector> &x, + std::vector> &residualOut) const { + sparseMatvec(matrix, x, residualOut); + for (std::size_t i = 0; i < rhs.size(); ++i) + for (unsigned c = 0; c < D; ++c) + residualOut[i][c] = rhs[i][c] - residualOut[i][c]; + } + + void multigridRestrict(const std::vector> &fineResidual, + const MultigridLevel &coarseLevel, + std::vector> &coarseRhs) const { + coarseRhs.assign(coarseLevel.indices.size(), zeroVec()); + for (std::size_t coarseId = 0; coarseId < coarseLevel.children.size(); + ++coarseId) { + const auto &children = coarseLevel.children[coarseId]; + if (children.empty()) + continue; + for (std::size_t fineId : children) + for (unsigned c = 0; c < D; ++c) + coarseRhs[coarseId][c] += fineResidual[fineId][c]; + const T scale = T(1) / static_cast(children.size()); + for (unsigned c = 0; c < D; ++c) + coarseRhs[coarseId][c] *= scale; + } + } + + void multigridProlongAdd(const std::vector> &coarseCorrection, + const MultigridLevel &coarseLevel, + std::vector> &fineCorrection) const { + for (std::size_t coarseId = 0; coarseId < coarseLevel.children.size(); + ++coarseId) { + for (std::size_t fineId : coarseLevel.children[coarseId]) + for (unsigned c = 0; c < D; ++c) + fineCorrection[fineId][c] += coarseCorrection[coarseId][c]; + } + } + + void multigridVCycle(const std::vector &levels, + std::size_t levelId, const std::vector> &rhs, + std::vector> &x, T smootherOmega) const { + const auto &level = levels[levelId]; + if (levelId + 1 >= levels.size() || level.indices.size() <= 32) { + multigridSmooth(level.matrix, rhs, x, 40, smootherOmega); + return; + } + + multigridSmooth(level.matrix, rhs, x, 2, smootherOmega); + + std::vector> fineResidual; + multigridResidual(level.matrix, rhs, x, fineResidual); + + std::vector> coarseRhs; + const auto &coarseLevel = levels[levelId + 1]; + multigridRestrict(fineResidual, coarseLevel, coarseRhs); + + std::vector> coarseCorrection(coarseRhs.size(), zeroVec()); + multigridVCycle(levels, levelId + 1, coarseRhs, coarseCorrection, + smootherOmega); + multigridProlongAdd(coarseCorrection, coarseLevel, x); + + multigridSmooth(level.matrix, rhs, x, 2, smootherOmega); + } + + std::vector> + multigridPrecondition(const std::vector &levels, + const std::vector> &rhs, + T smootherOmega) const { + std::vector> correction(rhs.size(), zeroVec()); + if (levels.empty()) + return correction; + multigridVCycle(levels, 0, rhs, correction, smootherOmega); + return correction; + } + + void exactElasticResidual(const SparseMatrix &matrix, + const std::vector> &x, + const std::vector> &b, + std::vector> &r) const { + std::vector> Ax(x.size(), zeroVec()); + sparseMatvec(matrix, x, Ax); + r.assign(x.size(), zeroVec()); + for (std::size_t i = 0; i < x.size(); ++i) + for (unsigned c = 0; c < D; ++c) + r[i][c] = b[i][c] - Ax[i][c]; + } + + static std::vector + solveUpperTriangular(const std::vector> &h, + const std::vector &g, unsigned usedColumns) { + std::vector y(usedColumns, T(0)); + for (int row = static_cast(usedColumns) - 1; row >= 0; --row) { + T sum = g[static_cast(row)]; + for (unsigned col = static_cast(row) + 1; col < usedColumns; + ++col) + sum -= h[static_cast(row)][col] * y[col]; + const T diag = + h[static_cast(row)][static_cast(row)]; + if (std::abs(diag) > std::numeric_limits::epsilon()) + y[static_cast(row)] = sum / diag; + } + return y; + } + + void solveElasticVelocity() { + if (parameters.contactMode > 0) { + solveElasticVelocityMultigridGMRES(); + return; + } + solveElasticVelocityBiCGSTAB(); + } + + void solveElasticVelocityMultigridGMRES() { + iterations = 0; + residual = 0.; + if (nodes.empty()) + return; + + const T lambda = lameLambda(); + const T mu = lameMu(); + const T gradDivWeight = + (lambda + mu) / + std::max(lambda + T(2) * mu, std::numeric_limits::epsilon()); + const T smootherOmega = + std::clamp(parameters.multigridSmootherOmega, T(0.2), T(1.4)); + + const std::size_t n = nodes.size(); + const std::vector> zeros(n, zeroVec()); + std::vector> b(n, zeroVec()); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) { + if (nodes[i].fixed) + b[i] = zeroVec(); + else + b[i] = computeElasticStencilAt(i, zeros, gradDivWeight); + } + + std::vector> x(n, zeroVec()); + for (std::size_t i = 0; i < n; ++i) + x[i] = nodes[i].fixed ? zeroVec() : nodes[i].velocity; + + // Rebuild the multigrid hierarchy only when the node count or the contact + // face classification changes. The stiffness matrix depends only on node + // geometry and which faces are active (compressive), not on the traction + // magnitudes β€” those only enter the load vector b computed above. Within + // a single time step the coupling iterations run without advection, so the + // mask geometry and contact pattern are typically stable after the first + // call and the matrix can be reused for all subsequent iterations. + const bool hierarchyDirty = + cachedNodeCount_ != n || cachedContactFaceActive_ != contactFaceActive_; + + if (hierarchyDirty) { + cachedMultigridLevels_ = buildMultigridHierarchy(); + if (cachedMultigridLevels_.empty()) + return; + cachedMultigridLevels_[0].matrix = + buildFineElasticMatrix(b, gradDivWeight); + for (std::size_t level = 1; level < cachedMultigridLevels_.size(); + ++level) + cachedMultigridLevels_[level].matrix = + buildGalerkinMatrix(cachedMultigridLevels_[level - 1].matrix, + cachedMultigridLevels_[level]); + cachedNodeCount_ = n; + cachedContactFaceActive_ = contactFaceActive_; + } + + if (cachedMultigridLevels_.empty()) + return; + const auto &multigridLevels = cachedMultigridLevels_; + + std::vector> r; + exactElasticResidual(multigridLevels[0].matrix, x, b, r); + + const T rhsNorm = vectorNorm(b); + const T absTolerance = + std::max(parameters.tolerance * rhsNorm, + std::numeric_limits::epsilon() * T(100) * + std::sqrt(static_cast( + std::max(std::size_t(1), n * D)))); + const T residualNormDenom = std::max(rhsNorm, absTolerance); + auto updateGmresResidual = [&](T absResidual) { + residual = (absResidual <= absTolerance) + ? T(0) + : absResidual / residualNormDenom; + }; + + T absResidual = vectorNorm(r); + updateGmresResidual(absResidual); + if (absResidual <= absTolerance || residual < parameters.tolerance) { + for (std::size_t i = 0; i < n; ++i) + nodes[i].velocity = x[i]; + return; + } + + constexpr unsigned restart = 32; + while (iterations < parameters.maxIterations && + residual > parameters.tolerance) { + const T beta = vectorNorm(r); + if (!std::isfinite(beta)) + throwNonFinite("traction multigrid GMRES residual"); + if (beta <= absTolerance) { + residual = T(0); + break; + } + + const unsigned innerLimit = + std::min(restart, parameters.maxIterations - iterations); + std::vector>> v(innerLimit + 1, + std::vector>(n, zeroVec())); + std::vector>> z(innerLimit, + std::vector>(n, zeroVec())); + v[0] = r; + vectorScale(v[0], T(1) / beta); + + std::vector> h(innerLimit + 1, + std::vector(innerLimit, T(0))); + std::vector cs(innerLimit, T(0)); + std::vector sn(innerLimit, T(0)); + std::vector g(innerLimit + 1, T(0)); + g[0] = beta; + + unsigned usedColumns = 0; + for (unsigned j = 0; j < innerLimit; ++j) { + z[j] = multigridPrecondition(multigridLevels, v[j], smootherOmega); + + std::vector> w(n, zeroVec()); + sparseMatvec(multigridLevels[0].matrix, z[j], w); + + for (unsigned i = 0; i <= j; ++i) { + h[i][j] = vectorDot(w, v[i]); + vectorSubtractInPlace(w, h[i][j], v[i]); + } + + h[j + 1][j] = vectorNorm(w); + if (h[j + 1][j] > std::numeric_limits::epsilon()) { + v[j + 1] = w; + vectorScale(v[j + 1], T(1) / h[j + 1][j]); + } + + for (unsigned i = 0; i < j; ++i) { + const T h0 = h[i][j]; + const T h1 = h[i + 1][j]; + h[i][j] = cs[i] * h0 + sn[i] * h1; + h[i + 1][j] = -sn[i] * h0 + cs[i] * h1; + } + + const T h0 = h[j][j]; + const T h1 = h[j + 1][j]; + const T denom = std::hypot(h0, h1); + if (denom <= std::numeric_limits::epsilon()) { + cs[j] = T(1); + sn[j] = T(0); + } else { + cs[j] = h0 / denom; + sn[j] = h1 / denom; + } + h[j][j] = cs[j] * h0 + sn[j] * h1; + h[j + 1][j] = T(0); + + const T g0 = g[j]; + g[j] = cs[j] * g0; + g[j + 1] = -sn[j] * g0; + + ++iterations; + usedColumns = j + 1; + const T projectedAbsResidual = std::abs(g[j + 1]); + updateGmresResidual(projectedAbsResidual); + if (!std::isfinite(residual)) + throwNonFinite("traction multigrid GMRES residual"); + if (projectedAbsResidual <= absTolerance || + residual < parameters.tolerance) + break; + } + + if (usedColumns == 0) + break; + + const auto y = solveUpperTriangular(h, g, usedColumns); + for (unsigned col = 0; col < usedColumns; ++col) + vectorAxpy(x, y[col], z[col]); + + exactElasticResidual(multigridLevels[0].matrix, x, b, r); + absResidual = vectorNorm(r); + updateGmresResidual(absResidual); + if (!std::isfinite(residual)) + throwNonFinite("traction multigrid GMRES residual"); + } + + for (std::size_t i = 0; i < n; ++i) + nodes[i].velocity = x[i]; + + if (residual > parameters.tolerance) + Logger::getInstance() + .addWarning("solveElasticVelocity: traction multigrid GMRES did not " + "converge after " + + std::to_string(iterations) + "/" + + std::to_string(parameters.maxIterations) + + " iterations (residual=" + std::to_string(residual) + + ", tolerance=" + std::to_string(parameters.tolerance) + + ")") + .print(); + } + + void solveElasticVelocityRelaxation() { + iterations = 0; + residual = 0.; + if (nodes.empty()) + return; + + const T lambda = lameLambda(); + const T mu = lameMu(); + const T gradDivWeight = + (lambda + mu) / + std::max(lambda + T(2) * mu, std::numeric_limits::epsilon()); + const T relaxation = std::clamp(parameters.relaxation, T(0.01), T(1)); + + std::vector> current(nodes.size()); + std::vector> next(nodes.size()); + for (std::size_t i = 0; i < nodes.size(); ++i) + current[i] = + nodes[i].fixed ? Vec3D{T(0), T(0), T(0)} : nodes[i].velocity; + + for (; iterations < parameters.maxIterations; ++iterations) { + T maxDelta = T(0); + T maxMagnitude = std::numeric_limits::epsilon(); + int finiteFlag = 1; + +#pragma omp parallel for schedule(static) \ + reduction(max : maxDelta, maxMagnitude) reduction(min : finiteFlag) + for (std::size_t i = 0; i < nodes.size(); ++i) { + Vec3D candidate = + nodes[i].fixed ? Vec3D{T(0), T(0), T(0)} + : computeElasticStencilAt(i, current, gradDivWeight); + + for (unsigned c = 0; c < D; ++c) { + if (!std::isfinite(candidate[c]) || !std::isfinite(current[i][c])) { + finiteFlag = 0; + next[i][c] = current[i][c]; + continue; + } + next[i][c] = + current[i][c] + relaxation * (candidate[c] - current[i][c]); + maxDelta = std::max(maxDelta, std::abs(next[i][c] - current[i][c])); + maxMagnitude = std::max(maxMagnitude, std::abs(next[i][c])); + maxMagnitude = std::max(maxMagnitude, std::abs(current[i][c])); + } + } + + if (!finiteFlag) { + residual = std::numeric_limits::infinity(); + throwNonFinite("traction mask solve"); + } + + residual = maxDelta / maxMagnitude; + current.swap(next); + if (residual < parameters.tolerance) { + ++iterations; + break; + } + } + + for (std::size_t i = 0; i < nodes.size(); ++i) + nodes[i].velocity = current[i]; + + if (residual > parameters.tolerance) + Logger::getInstance() + .addWarning("solveElasticVelocity: traction relaxation did not " + "converge after " + + std::to_string(iterations) + "/" + + std::to_string(parameters.maxIterations) + + " iterations (residual=" + std::to_string(residual) + + ", tolerance=" + std::to_string(parameters.tolerance) + + ")") + .print(); + } + + void solveElasticVelocityBiCGSTAB() { + iterations = 0; + residual = 0.; + if (nodes.empty()) + return; + + using SolverT = float; + + const T lambda = lameLambda(); + const T mu = lameMu(); + const T gradDivWeight = + (lambda + mu) / + std::max(lambda + T(2) * mu, std::numeric_limits::epsilon()); + + const std::size_t n = nodes.size(); + const Vec3D zero3{SolverT(0), SolverT(0), SolverT(0)}; + + // b[i] = F(0)[i]: contact BC constants (reaction/contact velocities). + // OOB and traction-free faces contribute v[i]=0 at zeros. + std::vector> b(n); + { + const std::vector> zeros(n, zero3); +#pragma omp parallel for schedule(static) + for (std::size_t i = 0; i < n; ++i) { + if (nodes[i].fixed) + b[i] = Vec3D{T(0), T(0), T(0)}; + else + b[i] = computeElasticStencilAt(i, zeros, gradDivWeight); + } + } + + // Cold start from zeros (matching original Jacobi behavior). + std::vector> x(n, zero3); + + // r = b - A*x. With x=0: A*0 = 0 - F(0) + b = 0, so r = b. + std::vector> r(n), r_hat(n); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + r[i][c] = static_cast(b[i][c]); + r_hat[i][c] = r[i][c]; + } + + // BiCGSTAB with identity preconditioner. + // For most interior nodes the diagonal of A is β‰ˆ 1; the identity + // preconditioner is exact there and a reasonable approximation at boundary + // nodes. + std::vector> pv(n, zero3), sv(n, zero3), y(n), z(n), s(n), + t(n); + T rho = T(1), alpha = T(1), omega = T(1); + + auto vecDot = [&](const std::vector> &a, + const std::vector> &bv) { + T sum = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + const T av = static_cast(a[i][c]); + const T bvVal = static_cast(bv[i][c]); + if (!std::isfinite(av) || !std::isfinite(bvVal)) + return std::numeric_limits::quiet_NaN(); + sum += av * bvVal; + } + return sum; + }; + + auto vecMaxAbs = [&](const std::vector> &vin) { + T m = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + const T value = static_cast(vin[i][c]); + if (!std::isfinite(value)) + return std::numeric_limits::infinity(); + m = std::max(m, std::abs(value)); + } + return m; + }; + + const T b_norm = [&] { + T m = T(0); + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + m = std::max(m, std::abs(b[i][c])); + return (m < T(1e-100)) ? T(1) : m; + }(); + + for (; iterations < parameters.maxIterations; ++iterations) { + const T rho_new = vecDot(r_hat, r); + if (!std::isfinite(rho_new) || std::abs(rho_new) < T(1e-100)) + break; + if (!std::isfinite(rho) || !std::isfinite(alpha) || + !std::isfinite(omega) || std::abs(omega) < T(1e-100)) + break; + + const T beta = (rho_new / rho) * (alpha / omega); + if (!std::isfinite(beta)) + break; + rho = rho_new; + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + pv[i][c] = static_cast(r[i][c] + + beta * (pv[i][c] - omega * sv[i][c])); + + // Identity preconditioner: y = p + y = pv; + elasticMatvec(y, b, gradDivWeight, sv); + + const T r_hat_v = vecDot(r_hat, sv); + if (!std::isfinite(r_hat_v) || std::abs(r_hat_v) < T(1e-100)) + break; + + alpha = rho_new / r_hat_v; + if (!std::isfinite(alpha)) + break; + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + s[i][c] = static_cast(r[i][c] - alpha * sv[i][c]); + + residual = vecMaxAbs(s); + if (!std::isfinite(residual)) + break; + if (residual < parameters.tolerance * b_norm) { + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + x[i][c] = static_cast(x[i][c] + alpha * y[i][c]); + ++iterations; + break; + } + + // Identity preconditioner: z = s + z = s; + elasticMatvec(z, b, gradDivWeight, t); + + const T t_s = vecDot(t, s); + const T t_t = vecDot(t, t); + if (!std::isfinite(t_s) || !std::isfinite(t_t) || t_t <= T(1e-100)) + break; + omega = t_s / t_t; + if (!std::isfinite(omega)) + break; + + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) { + x[i][c] = + static_cast(x[i][c] + alpha * y[i][c] + omega * z[i][c]); + r[i][c] = static_cast(s[i][c] - omega * t[i][c]); + } + + residual = vecMaxAbs(r); + if (!std::isfinite(residual)) + break; + if (residual < parameters.tolerance * b_norm) { + ++iterations; + break; + } + } + + bool finiteSolution = true; + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + if (!std::isfinite(static_cast(x[i][c]))) + finiteSolution = false; + + if (finiteSolution) { + for (std::size_t i = 0; i < n; ++i) + for (unsigned c = 0; c < D; ++c) + nodes[i].velocity[c] = static_cast(x[i][c]); + } else { + residual = std::numeric_limits::infinity(); + throwNonFinite("legacy kinematic mask solve"); + } + if (residual > parameters.tolerance * b_norm) + Logger::getInstance() + .addWarning( + "solveElasticVelocity: BiCGSTAB did not converge after " + + std::to_string(iterations) + "/" + + std::to_string(parameters.maxIterations) + + " iterations (residual=" + std::to_string(residual / b_norm) + + ", tolerance=" + std::to_string(parameters.tolerance) + ")") + .print(); + } + + bool touchesContactBoundary(ConstSparseIterator &maskIt, + const IndexType &index) const { + for (unsigned direction = 0; direction < D; ++direction) { + for (int offset : {-1, 1}) { + IndexType neighbor = index; + neighbor[direction] += offset; + if (!inBounds(neighbor)) { + // Solve region is clipped to the mask/oxide interface: an + // out-of-bounds neighbor is by definition outside the mask. + if (isContactBoundary(index, direction, offset, maskIt)) + return true; + continue; + } + if (lookupNode(neighbor) != noNode) + continue; + if (!crosses(valueAt(maskIt, index), valueAt(maskIt, neighbor))) + continue; + if (isContactBoundary(index, direction, offset, maskIt)) + return true; + } + } + return false; + } + + bool isContactBoundary(const IndexType &index, unsigned direction, int offset, + ConstSparseIterator &maskIt) const { + const T grad = maskGradientComponent(index, direction, maskIt); + + // The face exits the selected bending domain only if it points from an + // inside node toward the outside of that domain. Use the signed level-set + // gradient instead of a normalized normal so HRLE far-field sentinels + // cannot produce a spurious zero normal. + if (static_cast(maskSign) * static_cast(offset) * grad >= T(0)) + return false; + + if (ambientInterface == nullptr) + return direction == D - 1; // no ambient LS: fall back to bottom-face only + + IndexType ghostIndex = index; + ghostIndex[direction] += offset; + return isInsideOxide(ghostIndex); + } + + bool isInsideOxide(const IndexType &index) const { + if (ambientInterface == nullptr) + return false; + const auto it = ambientPhiCache_.find(detail::gridIndexHash(index)); + return it != ambientPhiCache_.end() && it->second >= T(0); + } + + T maskFaceDistance(ConstSparseIterator &maskIt, const IndexType &inside, + const IndexType &outside) const { + if (!inBounds(outside)) + return gridDelta; + return crossingDistance(valueAt(maskIt, inside), valueAt(maskIt, outside)); + } + + void markFixedNodes() { + fixedNodes = 0; + if (nodes.empty() || parameters.anchorBoundarySide == 0) + return; + if (parameters.anchorBoundaryDirection < 0 || + parameters.anchorBoundaryDirection >= D) + return; + + const unsigned dir = + static_cast(parameters.anchorBoundaryDirection); + + // Warn if the anchor is placed along the oxide-growth direction (D-1 in a + // standard LOCOS cross-section is the vertical/y axis). Clamping the top + // or bottom of the mask instead of a far lateral edge removes the degrees + // of freedom the bending solve needs and will suppress the bird's-beak + // deflection entirely. + if (dir == static_cast(D - 1)) + Logger::getInstance() + .addWarning("OxidationMaskBending: anchorBoundaryDirection=" + + std::to_string(parameters.anchorBoundaryDirection) + + " points along the oxide-growth axis (direction D-1=" + + std::to_string(D - 1) + + "). The anchor is normally " + "placed at a lateral edge (direction 0) so bending " + "degrees of freedom are preserved. Set " + "anchorBoundaryDirection=0 for a LOCOS cross-section.") + .print(); + IndexType nodeMin = nodes.front().index; + IndexType nodeMax = nodes.front().index; + for (const auto &node : nodes) { + for (unsigned d = 0; d < D; ++d) { + nodeMin[d] = std::min(nodeMin[d], node.index[d]); + nodeMax[d] = std::max(nodeMax[d], node.index[d]); + } + } + + const auto layers = static_cast( + std::max(1u, parameters.anchorBoundaryLayers)); + for (auto &node : nodes) { + const bool onLower = parameters.anchorBoundarySide < 0 && + node.index[dir] <= nodeMin[dir] + layers - 1; + const bool onUpper = parameters.anchorBoundarySide > 0 && + node.index[dir] >= nodeMax[dir] - layers + 1; + if (onLower || onUpper) { + node.fixed = true; + node.velocity = {T(0), T(0), T(0)}; + ++fixedNodes; + } + } + } + + T maskGradientComponent(const IndexType &index, unsigned direction, + ConstSparseIterator &maskIt) const { + IndexType pos = index; + IndexType neg = index; + pos[direction] += 1; + neg[direction] -= 1; + if (!inBounds(pos)) + pos = index; + if (!inBounds(neg)) + neg = index; + return detail::clampLevelSetPhi(valueAt(maskIt, pos)) - + detail::clampLevelSetPhi(valueAt(maskIt, neg)); + } + + T clampedPoissonRatio() const { + return std::clamp(parameters.poissonRatio, T(-0.95), T(0.49)); + } + + static constexpr T gasConstant = T(8.31446261815324); + + bool isElasticContactMode() const { return parameters.contactMode == 2; } + + bool usesKinematicContactBoundary() const { + return parameters.contactMode == 0 || isElasticContactMode(); + } + + T effectiveMaskViscosity() const { + // Elastic mode: Ξ·_eff = E (PaΒ·hr, with implicit dt_ref = 1 hr). + // lameMu/lameLambda equal the standard elastic LamΓ© constants G and Ξ». + // Contact faces use kinematic (Dirichlet) BC: v_contact = v_oxide Γ— dt, + // so the solver gives u_new β‰ˆ v_oxide Γ— dt (displacement in Β΅m stored as + // Β΅m/hr). finalizeElasticAdvectionVelocity() converts u_new β†’ u_new/dt + // (the actual advection velocity Β΅m/hr). Displacement feedback into the + // oxide comes through the outer coupling loop in lsOxidation. + if (isElasticContactMode()) + return parameters.youngModulus; // PaΒ·hr with implicit dt_ref = 1 hr + const T temperature = std::max(parameters.temperature, T(1.)); + const T referenceTemperature = + std::max(parameters.referenceTemperature, T(1.)); + return parameters.referenceViscosity * + std::exp(parameters.creepActivationEnergy / gasConstant * + (T(1) / temperature - T(1) / referenceTemperature)); + } + + // LamΓ© viscosity parameters: same structure as elastic LamΓ© constants but + // with eta(T) in place of Young's modulus, making the governing equation a + // viscous Stokes flow rather than elastic equilibrium. + T lameMu() const { + const T nu = clampedPoissonRatio(); + return effectiveMaskViscosity() / (T(2) * (T(1) + nu)); + } + + T lameLambda() const { + const T nu = clampedPoissonRatio(); + return effectiveMaskViscosity() * nu / ((T(1) + nu) * (T(1) - T(2) * nu)); + } + + Vec3D getVelocity(const IndexType &index) const { + const std::size_t nodeId = lookupNode(index); + if (nodeId != noNode) + return nodes[nodeId].velocity; + + const auto nearby = findNearbyNode(index); + if (nearby == noNode) + return {0., 0., 0.}; + return nodes[nearby].velocity; + } + + bool isInsideMask(ConstSparseIterator &maskIt, const IndexType &index) const { + return maskSign * valueAt(maskIt, index) >= 0.; + } + + T crossingDistance(T insidePhi, T outsidePhi) const { + return detail::levelSetCrossingDistance( + insidePhi, outsidePhi, parameters.minBoundaryDistance, gridDelta); + } +}; + +/// Velocity field for the oxide outer interface when part of that interface is +/// in contact with a mask. Open oxide/ambient regions use the oxide deformation +/// field. Mask-contact regions are kinematically constrained to the solved mask +/// vector velocity and do not receive additional free-surface scalar growth. +template +class OxidationConstrainedAmbient final : public VelocityField { + using IndexType = viennahrle::Index; + using ConstSparseIterator = + viennahrle::ConstSparseIterator::DomainType>; + + SmartPointer> deformationField = nullptr; + SmartPointer> maskVelocityField = nullptr; + SmartPointer> maskInterface = nullptr; + SmartPointer> ambientInterface = nullptr; + int maskSign = 1; + std::unordered_map maskPhiCache_; + T maskGridDelta_ = 1.; + std::array maxVelocity_{}; + +public: + OxidationConstrainedAmbient() = default; + + OxidationConstrainedAmbient( + SmartPointer> passedDeformation, + SmartPointer> passedMaskVelocity, + SmartPointer> passedMaskInterface, int passedMaskSign = 1) + : deformationField(passedDeformation), + maskVelocityField(passedMaskVelocity), + maskInterface(passedMaskInterface), + maskSign((passedMaskSign < 0) ? -1 : 1) { + buildMaskPhiCache(); + buildEffectiveVelocityCache(); + } + + OxidationConstrainedAmbient( + SmartPointer> passedDeformation, + SmartPointer> passedMaskVelocity, + SmartPointer> passedMaskInterface, + SmartPointer> passedAmbientInterface, int passedMaskSign = 1) + : deformationField(passedDeformation), + maskVelocityField(passedMaskVelocity), + maskInterface(passedMaskInterface), + ambientInterface(passedAmbientInterface), + maskSign((passedMaskSign < 0) ? -1 : 1) { + buildMaskPhiCache(); + buildEffectiveVelocityCache(); + } + + template static auto New(Args &&...args) { + return SmartPointer::New( + std::forward(args)...); + } + + Vec3D getVectorVelocity(const Vec3D &coordinate, int material, + const Vec3D &normalVector, + unsigned long pointId) final { + const T signedPhi = getSignedMaskPhi(coordinate); + + // At/inside mask surface: kinematic constraint β€” oxide tracks mask exactly. + if (signedPhi >= T(0) && maskVelocityField != nullptr) + return maskVelocityField->getVectorVelocity(coordinate, material, + normalVector, pointId); + if (deformationField == nullptr) + return {0., 0., 0.}; + + auto v_def = deformationField->getVectorVelocity(coordinate, material, + normalVector, pointId); + + // Near-contact gap zone: smoothly approach the mask velocity instead of + // letting stress spikes just outside the mask edge advect the ambient level + // set with the full oxide deformation velocity. + if (maskVelocityField != nullptr) { + if (signedPhi > -T(3) * maskGridDelta_) { + auto v_mask = maskVelocityField->getVectorVelocity( + coordinate, material, normalVector, pointId); + const T blend = + std::max(T(0), std::min(T(1), (signedPhi + T(3) * maskGridDelta_) / + (T(3) * maskGridDelta_))); + for (int k = 0; k < D; ++k) + v_def[k] = (T(1) - blend) * v_def[k] + blend * v_mask[k]; + + T def_n = T(0), mask_n = T(0); + for (int k = 0; k < D; ++k) { + def_n += v_def[k] * normalVector[k]; + mask_n += v_mask[k] * normalVector[k]; + } + if (mask_n > def_n) { + const T boost = mask_n - def_n; + Vec3D v_out = v_def; + for (int k = 0; k < D; ++k) + v_out[k] += boost * normalVector[k]; + return v_out; + } + } + } + return v_def; + } + + T getScalarVelocity(const Vec3D &coordinate, int material, + const Vec3D &normalVector, + unsigned long pointId) final { + if (getSignedMaskPhi(coordinate) >= T(0)) + return 0.; + if (deformationField == nullptr) + return 0.; + return deformationField->getScalarVelocity(coordinate, material, + normalVector, pointId); + } + + T getDissipationAlpha(int direction, int material, + const Vec3D ¢ralDifferences) final { + if (direction < 0 || direction >= static_cast(D)) + return T(0); + (void)material; + (void)centralDifferences; + return maxVelocity_[direction]; + } + +private: + // Returns maskSign * maskPhi at the grid node nearest to coordinate. + // Positive β†’ inside mask (contact), negative β†’ outside mask (gap or free). + // Uses a pre-built cache so no HRLE iterator is constructed in the hot path. + // Nodes outside the narrow band return lowest() (unambiguously outside mask). + T getSignedMaskPhi(const Vec3D &coordinate) const { + if (maskInterface == nullptr) + return std::numeric_limits::lowest(); + IndexType index; + for (unsigned i = 0; i < D; ++i) + index[i] = std::llround(coordinate[i] / maskGridDelta_); + const auto it = maskPhiCache_.find(detail::gridIndexHash(index)); + return (it != maskPhiCache_.end()) ? it->second + : std::numeric_limits::lowest(); + } + + void buildMaskPhiCache() { + maskPhiCache_.clear(); + if (maskInterface == nullptr) + return; + maskGridDelta_ = maskInterface->getGrid().getGridDelta(); + for (ConstSparseIterator it(maskInterface->getDomain()); !it.isFinished(); + ++it) { + if (!it.isDefined()) + continue; + const auto key = detail::gridIndexHash(it.getStartIndices()); + maskPhiCache_[key] = static_cast(maskSign) * it.getValue(); + } + } + + void buildEffectiveVelocityCache() { + maxVelocity_.fill(T(0)); + if (ambientInterface == nullptr) { + for (unsigned d = 0; d < D; ++d) { + if (deformationField != nullptr) + maxVelocity_[d] = + std::max(maxVelocity_[d], + deformationField->getDissipationAlpha(d, -1, {})); + if (maskVelocityField != nullptr) + maxVelocity_[d] = + std::max(maxVelocity_[d], + maskVelocityField->getDissipationAlpha(d, -1, {})); + } + return; + } + + const T gridDelta = ambientInterface->getGrid().getGridDelta(); + bool foundInterfacePoint = false; + viennahrle::ConstSparseStarIterator::DomainType, 1> + neighborIterator(ambientInterface->getDomain()); + for (ConstSparseIterator it(ambientInterface->getDomain()); + !it.isFinished(); ++it) { + if (!it.isDefined() || std::abs(it.getValue()) > T(1)) + continue; + foundInterfacePoint = true; + + const auto index = it.getStartIndices(); + neighborIterator.goToIndicesSequential(index); + + Vec3D coordinate{0., 0., 0.}; + Vec3D normal{0., 0., 0.}; + T normalNorm2 = T(0); + for (unsigned d = 0; d < D; ++d) { + coordinate[d] = static_cast(index[d]) * gridDelta; + normal[d] = neighborIterator.getNeighbor(d).getValue() - + neighborIterator.getNeighbor(d + D).getValue(); + normalNorm2 += normal[d] * normal[d]; + } + if (normalNorm2 > std::numeric_limits::epsilon()) { + const T invNorm = T(1) / std::sqrt(normalNorm2); + for (unsigned d = 0; d < D; ++d) + normal[d] *= invNorm; + } + + const auto vectorVelocity = getVectorVelocity(coordinate, -1, normal, 0); + const T scalarVelocity = getScalarVelocity(coordinate, -1, normal, 0); + for (unsigned d = 0; d < D; ++d) { + maxVelocity_[d] = + std::max(maxVelocity_[d], + std::abs(vectorVelocity[d] + scalarVelocity * normal[d])); + } + } + + if (!foundInterfacePoint) { + for (unsigned d = 0; d < D; ++d) { + if (deformationField != nullptr) + maxVelocity_[d] = + std::max(maxVelocity_[d], + deformationField->getDissipationAlpha(d, -1, {})); + if (maskVelocityField != nullptr) + maxVelocity_[d] = + std::max(maxVelocity_[d], + maskVelocityField->getDissipationAlpha(d, -1, {})); + } + } + } +}; + +} // namespace viennals diff --git a/include/viennals/lsOxidationModel.hpp b/include/viennals/lsOxidationModel.hpp new file mode 100644 index 00000000..90092cf7 --- /dev/null +++ b/include/viennals/lsOxidationModel.hpp @@ -0,0 +1,250 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +namespace viennals { + +struct OxidationCouplingParameters { + unsigned maxIterations = 10; + double tolerance = 1e-6; + double relaxation = 1.; +}; + +/// Iterates diffusion, oxide deformation, and pressure-dependent reaction-rate +/// feedback on the shared Cartesian solve grid. +template class OxidationModel { + using IndexType = viennahrle::Index; + + SmartPointer> diffusionField = nullptr; + SmartPointer> deformationField = nullptr; + OxidationCouplingParameters parameters; + IndexType minIndex{}; + IndexType maxIndex{}; + bool useRequestedBounds = false; + unsigned iterations = 0; + T residual = std::numeric_limits::max(); + bool converged_ = false; + std::string failureReason_; + +public: + OxidationModel() = default; + + OxidationModel( + SmartPointer> passedDiffusionField, + SmartPointer> passedDeformationField, + OxidationCouplingParameters passedParameters = {}) + : diffusionField(passedDiffusionField), + deformationField(passedDeformationField), parameters(passedParameters) { + } + + template static auto New(Args &&...args) { + return SmartPointer::New(std::forward(args)...); + } + + void setDiffusionField( + SmartPointer> passedDiffusionField) { + diffusionField = passedDiffusionField; + } + + void setDeformationField( + SmartPointer> passedDeformationField) { + deformationField = passedDeformationField; + } + + void setParameters(OxidationCouplingParameters passedParameters) { + parameters = passedParameters; + } + + void setSolveBounds(const IndexType &passedMinIndex, + const IndexType &passedMaxIndex) { + minIndex = passedMinIndex; + maxIndex = passedMaxIndex; + useRequestedBounds = true; + } + + void clearSolveBounds() { useRequestedBounds = false; } + + void apply() { + iterations = 0; + residual = 0.; + converged_ = false; + failureReason_.clear(); + if (diffusionField == nullptr || deformationField == nullptr) { + Logger::getInstance() + .addError("OxidationModel: Missing diffusion or deformation " + "field.") + .print(); + return; + } + + if (useRequestedBounds) { + diffusionField->setSolveBounds(minIndex, maxIndex); + deformationField->setSolveBounds(minIndex, maxIndex); + } else { + diffusionField->clearSolveBounds(); + deformationField->clearSolveBounds(); + } + + // forEachSolutionNode order is deterministic on a fixed grid, so + // nodeIndices is collected once and p_raw is refilled in-place on + // subsequent iterations. + std::vector nodeIndices; + std::vector p_raw; + std::vector p_km1, p_k; // Aitken Δ² history + + for (; iterations < parameters.maxIterations; ++iterations) { + VIENNACORE_LOG_DEBUG("OxidationModel: coupling iteration " + + std::to_string(iterations + 1) + "/" + + std::to_string(parameters.maxIterations) + + " starting diffusion solve"); + diffusionField->apply(); + VIENNACORE_LOG_DEBUG( + "OxidationModel: diffusion solve complete, nodes=" + + std::to_string(diffusionField->getNumberOfSolutionNodes()) + + ", iterations=" + std::to_string(diffusionField->getIterations()) + + ", residual=" + std::to_string(diffusionField->getResidual())); + if (!diffusionField->lastSolveConverged() || + !diffusionField->hasFiniteConcentrationField()) { + residual = std::numeric_limits::infinity(); + failureReason_ = + "diffusion solve failed (residual=" + + std::to_string(diffusionField->getNormalizedResidual()) + + ", tolerance=" + + std::to_string(diffusionField->getParameters().tolerance) + ")"; + Logger::getInstance() + .addWarning("OxidationModel: " + failureReason_ + ".") + .print(); + return; + } + + VIENNACORE_LOG_DEBUG("OxidationModel: coupling iteration " + + std::to_string(iterations + 1) + "/" + + std::to_string(parameters.maxIterations) + + " starting deformation solve"); + Timer<> tDeform; + tDeform.start(); + deformationField->apply(); + tDeform.finish(); + VIENNACORE_LOG_DEBUG( + "OxidationModel: deformation solve complete, nodes=" + + std::to_string(deformationField->getNumberOfSolutionNodes()) + + ", iterations=" + std::to_string(deformationField->getIterations()) + + ", residual=" + std::to_string(deformationField->getResidual())); + if (!deformationField->lastSolveConverged() || + !deformationField->hasFiniteSolution()) { + residual = std::numeric_limits::infinity(); + failureReason_ = + "deformation solve failed (mechanics=" + + std::to_string(deformationField->getResidual()) + ", pressure=" + + std::to_string(deformationField->getLastPressureResidual()) + + ", stokes=" + + std::to_string(deformationField->getLastStokesResidual()) + ")"; + VIENNACORE_LOG_WARNING("OxidationModel: " + failureReason_ + "."); + return; + } + if (Logger::hasTiming()) + Logger::getInstance() + .addTiming(" deformation(couplingIter=" + + std::to_string(iterations + 1) + ")", + tDeform) + .print(); + + // Collect raw pressures G(x_k) from deformation. + if (iterations == 0) { + nodeIndices.clear(); + p_raw.clear(); + deformationField->forEachSolutionNode([&](const IndexType &idx, T p) { + nodeIndices.push_back(idx); + p_raw.push_back(p); + }); + } else { + std::size_t ii = 0; + deformationField->forEachSolutionNode( + [&](const IndexType &, T p) { p_raw[ii++] = p; }); + } + const std::size_t n = p_raw.size(); + if (!std::all_of(p_raw.begin(), p_raw.end(), + [](T value) { return std::isfinite(value); })) { + residual = std::numeric_limits::infinity(); + failureReason_ = + "deformation pressure feedback produced non-finite values"; + VIENNACORE_LOG_WARNING("OxidationModel: " + failureReason_ + "."); + return; + } + + // Aitken Δ²: find ΞΈ minimising ||(1-ΞΈ)F_{k-1} + ΞΈF_k||Β². + // Clamped to [0.1, 1.0]: no extrapolation (ΞΈ>1) because the + // pressureβ†’concentration feedback is nonlinear (exponential kinetics) + // and overshooting the pressure causes oscillatory divergence. + T aitkenTheta = T(1); + if (p_k.size() == n && p_km1.size() == n) { + T dot_Fkm1_dF = T(0), dot_dF_dF = T(0); + for (std::size_t i = 0; i < n; ++i) { + const T Fkm1 = p_k[i] - p_km1[i]; + const T Fk = p_raw[i] - p_k[i]; + const T dF = Fk - Fkm1; + dot_Fkm1_dF += Fkm1 * dF; + dot_dF_dF += dF * dF; + } + if (dot_dF_dF > T(1e-100)) + aitkenTheta = + std::max(T(0.1), std::min(T(1.0), -dot_Fkm1_dF / dot_dF_dF)); + } + + // Compute blended pressure and relative-change residual. + std::vector p_blended(n); + T maxChange = T(0), maxPressure = T(0); + for (std::size_t i = 0; i < n; ++i) { + const T oldP = p_k.empty() ? T(0) : p_k[i]; + p_blended[i] = (T(1) - aitkenTheta) * oldP + aitkenTheta * p_raw[i]; + maxChange = std::max(maxChange, std::abs(p_blended[i] - oldP)); + maxPressure = std::max(maxPressure, std::abs(p_blended[i])); + } + residual = (maxPressure > std::numeric_limits::epsilon()) + ? maxChange / maxPressure + : maxChange; + + // Feed blended pressures to diffusion. + for (std::size_t i = 0; i < n; ++i) + diffusionField->setPressure(nodeIndices[i], p_blended[i]); + + // Shift Aitken history; seed p_km1 with zeros on the first iteration. + p_km1 = p_k.empty() ? std::vector(n, T(0)) : std::move(p_k); + p_k = p_blended; + + VIENNACORE_LOG_DEBUG( + "OxidationModel: coupling iteration " + + std::to_string(iterations + 1) + + " pressure-feedback residual=" + std::to_string(residual)); + if (residual < parameters.tolerance) + break; + } + const unsigned completedIterations = + std::min(iterations + 1, parameters.maxIterations); + VIENNACORE_LOG_DEBUG("OxidationModel: coupled solve complete, iterations=" + + std::to_string(completedIterations) + + ", residual=" + std::to_string(residual)); + converged_ = std::isfinite(residual) && residual <= parameters.tolerance; + if (!converged_) + VIENNACORE_LOG_WARNING( + "OxidationModel: pressure-concentration coupling did not " + "converge after " + + std::to_string(completedIterations) + + " iterations (residual=" + std::to_string(residual) + + ", tolerance=" + std::to_string(parameters.tolerance) + + "). Consider increasing maxIterations or relaxation."); + } + + unsigned getIterations() const { return iterations; } + T getResidual() const { return residual; } + bool hasConverged() const { return converged_; } + std::string getFailureReason() const { return failureReason_; } +}; + +} // namespace viennals diff --git a/include/viennals/lsOxidationPresets.hpp b/include/viennals/lsOxidationPresets.hpp new file mode 100644 index 00000000..96c1bd63 --- /dev/null +++ b/include/viennals/lsOxidationPresets.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include + +namespace viennals { + +/// Named parameter presets for common thermal oxidation processes. +/// +/// Each method returns a fully populated parameter struct for a specific, +/// documented process condition. The names encode the condition deliberately β€” +/// a user calibrating for a different temperature or ambient should supply +/// their own struct rather than modifying these. +/// +/// Sources: +/// Deal-Grove coefficients β€” B. E. Deal and A. S. Grove, J. Appl. Phys. 36, +/// 3770 (1965); wet-oxidation values at 1000 Β°C from Table I. +/// Oxide mechanics β€” E. A. Irene, J. Electrochem. Soc. 125, 1708 (1978); +/// viscosity and elastic moduli for thermal SiO2 near 1000 Β°C. +/// Silicon nitride creep β€” S. M. Hu, J. Appl. Phys. 70, R53 (1991); +/// reference viscosity for LPCVD Si3N4 at 1000 Β°C. +struct OxidationPresets { + /// Wet oxidation at 1000 Β°C (Deal-Grove linear-parabolic coefficients). + /// B/2 = D_eff = 0.157 Β΅mΒ²/hr, B/A = k_s = 0.74 Β΅m/hr. + /// Stress-coupling activation volumes from Kao et al. (1987). + static OxidationParameters wet1000CDealGrove100() { + OxidationParameters params; + params.diffusionCoefficient = 0.157; // Β΅mΒ²/hr, B/2 = D_eff + params.reactionRate = 0.74; // Β΅m/hr, B/A = k_s + params.transferCoefficient = 100.; // large β†’ surface conc. β‰ˆ C* + params.equilibriumConcentration = 1.; + params.oxidantMoleculeDensity = 1.; + params.expansionCoefficient = 2.27; // SiO2/Si volume ratio + params.temperature = 1273.15; // 1000 Β°C in K + params.reactionActivationVolume = 1.76e-35; // mΒ³, stress activation for k_s + params.diffusionActivationVolume = 0.; + return params; + } + + /// Thermal SiO2 viscoelastic mechanics at 1000 Β°C. + /// Viscosity ~1Γ—10¹⁰ PaΒ·hr (Irene 1978), shear modulus ~3Γ—10¹⁰ Pa. + /// Pass the advection time step so the viscoelastic relaxation is consistent. + static OxidationDeformationParameters oxideMechanics1000C(double timeStep) { + OxidationDeformationParameters params; + params.viscosity = 1.e10; // PaΒ·hr, effective oxide viscosity + params.bulkModulus = 7.5e8; // Pa + params.shearModulus = 3.e10; // Pa + params.stressTimeStep = timeStep; + params.mechanicsIterations = 300; + params.mechanicsTolerance = 5.e-3; + params.pressureIterations = 2000; + params.stokesIterations = 1000; + params.pressureTolerance = 1.e-3; + params.stokesTolerance = 1.e-3; + params.tolerance = 1.e-7; + return params; + } + + /// LPCVD Si3N4 mask creep at 1000 Β°C (Hu 1991; Senez et al. 1994). + /// referenceViscosity = 5Γ—10ΒΉΒΉ PaΒ·hr at 1273.15 K with Arrhenius scaling: + /// E_a = 3.86Γ—10⁡ J/mol β‰ˆ 4 eV (Senez, IEEE Trans. Electron Devices, 1994). + /// Set temperature in OxidationMaskParameters (or via psOxidation) to get + /// the correct viscosity at a temperature other than 1000 Β°C. + static OxidationMaskParameters siliconNitrideMask1000C() { + OxidationMaskParameters params; + params.temperature = 1273.15; + params.referenceTemperature = 1273.15; + params.referenceViscosity = 5.e11; // PaΒ·hr at 1000 Β°C + params.creepActivationEnergy = 3.86e5; // J/mol β‰ˆ 4 eV (Senez 1994) + params.poissonRatio = 0.27; + params.relaxation = 0.9; + params.tolerance = 5.e-6; + return params; + } +}; + +} // namespace viennals diff --git a/include/viennals/lsOxidationSolverBase.hpp b/include/viennals/lsOxidationSolverBase.hpp new file mode 100644 index 00000000..5554e5f5 --- /dev/null +++ b/include/viennals/lsOxidationSolverBase.hpp @@ -0,0 +1,378 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace viennals { + +using namespace viennacore; + +namespace detail { + +template +inline std::size_t gridIndexHash(const viennahrle::Index &index) { + std::size_t seed = 0; + for (unsigned i = 0; i < static_cast(D); ++i) { + seed ^= std::hash{}(static_cast(index[i])) + + std::size_t(0x9e3779b97f4a7c15ULL) + (seed << 6) + (seed >> 2); + } + return seed; +} + +// Hash functor for viennahrle::Index β€” safe to use as unordered_map key +// because collisions are resolved by operator== on the full index. +template struct IndexTypeHasher { + std::size_t operator()(const viennahrle::Index &idx) const { + return gridIndexHash(idx); + } +}; + +template inline Vec3D vecScaled(const Vec3D &source, T factor) { + Vec3D result{0., 0., 0.}; + for (unsigned i = 0; i < 3; ++i) + result[i] = source[i] * factor; + return result; +} + +template +inline Vec3D vecAdd(const Vec3D &a, const Vec3D &b) { + Vec3D result{0., 0., 0.}; + for (unsigned i = 0; i < 3; ++i) + result[i] = a[i] + b[i]; + return result; +} + +template +inline Vec3D vecSubtract(const Vec3D &a, const Vec3D &b) { + Vec3D result{0., 0., 0.}; + for (unsigned i = 0; i < 3; ++i) + result[i] = a[i] - b[i]; + return result; +} + +template +inline void vecAddTo(Vec3D &target, const Vec3D &source) { + for (unsigned i = 0; i < 3; ++i) + target[i] += source[i]; +} + +/// Clamp HRLE far-field sentinels (Β±DBL_MAX) to Β±1 before differencing to +/// prevent DBL_MAXΒ² overflow that silently returns the zero vector. +template inline T clampLevelSetPhi(T v) { + return v > T(1) ? T(1) : (v < T(-1) ? T(-1) : v); +} + +template +inline T levelSetCrossingDistance(T insidePhi, T outsidePhi, + T minBoundaryFraction, T gridDelta) { + const T denom = std::abs(insidePhi) + std::abs(outsidePhi); + if (denom <= std::numeric_limits::epsilon()) + return gridDelta; + return std::max(minBoundaryFraction * gridDelta, + gridDelta * std::abs(insidePhi) / denom); +} + +} // namespace detail + +/// Common Cartesian-grid infrastructure shared by the three oxidation solver +/// classes (diffusion, deformation, mask bending). Provides the node lookup +/// table, grid extents, and the core grid utility methods that operate on them. +template class OxidationSolverBase { +protected: + using IndexType = viennahrle::Index; + using ConstSparseIterator = + viennahrle::ConstSparseIterator::DomainType>; + + static constexpr std::size_t noNode = std::numeric_limits::max(); + std::vector nodeLookupFlat; + IndexType minIndex{}; + IndexType maxIndex{}; + std::array extents{}; + std::array strides{}; + T gridDelta = 1.; + + struct GridBounds { + IndexType min{}; + IndexType max{}; + bool foundDefinedPoints = false; + }; + + bool crosses(T a, T b) const { + // Use a small tolerance consistent with isInsideOxide so that grid points + // that land exactly on an interface (phi β‰ˆ 0 from floating-point rounding + // in GeometricAdvect) are treated as surface points in boundary detection. + constexpr T eps = T(1e-9); + return (a <= eps && b >= -eps) || (a >= -eps && b <= eps); + } + + T valueAt(ConstSparseIterator &it, const IndexType &index) const { + it.goToIndices(index); + return it.getValue(); + } + + bool inBounds(const IndexType &index) const { + for (unsigned i = 0; i < D; ++i) + if (index[i] < minIndex[i] || index[i] > maxIndex[i]) + return false; + return true; + } + + void initNodeLookup() { + std::size_t total = 1; + for (unsigned i = 0; i < D; ++i) + total *= extents[i]; + nodeLookupFlat.assign(total, noNode); + } + + std::size_t lookupNode(const IndexType &index) const { + if (!inBounds(index)) + return noNode; + return nodeLookupFlat[linearIndex(index)]; + } + + std::size_t linearIndex(const IndexType &index) const { + if (!inBounds(index)) + return std::numeric_limits::max(); + std::size_t result = 0; + for (unsigned i = 0; i < D; ++i) + result += static_cast(index[i] - minIndex[i]) * strides[i]; + return result; + } + + bool increment(IndexType &index) const { + for (unsigned i = 0; i < D; ++i) { + if (index[i] < maxIndex[i]) { + ++index[i]; + return true; + } + index[i] = minIndex[i]; + } + return false; + } + + // Searches with expanding radius shells so that RK2 Stage 2 can find oxide + // nodes even after the surface has advanced several grid cells beyond the + // band. + std::size_t findNearbyNode(const IndexType &index) const { + for (int radius = 1; radius <= 4; ++radius) { + T bestDistance2 = std::numeric_limits::max(); + std::size_t bestNode = std::numeric_limits::max(); + + IndexType offset{}; + offset.fill(-radius); + while (true) { + IndexType candidate = index; + T distance2 = 0.; + for (unsigned i = 0; i < D; ++i) { + candidate[i] += offset[i]; + distance2 += static_cast(offset[i] * offset[i]); + } + + if (distance2 > 0 && inBounds(candidate)) { + const std::size_t foundId = nodeLookupFlat[linearIndex(candidate)]; + if (foundId != noNode && distance2 < bestDistance2) { + bestDistance2 = distance2; + bestNode = foundId; + } + } + + unsigned dim = 0; + for (; dim < D; ++dim) { + if (offset[dim] < radius) { + ++offset[dim]; + break; + } + offset[dim] = -radius; + } + if (dim == D) + break; + } + + if (bestNode != std::numeric_limits::max()) + return bestNode; + } + return std::numeric_limits::max(); + } + + bool initializeGridFromInterfaces( + SmartPointer> reactionInterface, + SmartPointer> ambientInterface, + SmartPointer> maskInterface, bool useRequestedBounds, + const IndexType &requestedMinIndex, const IndexType &requestedMaxIndex, + std::size_t maxGridPoints, const std::string &solverName) { + auto &reactionGrid = reactionInterface->getGrid(); + auto &ambientGrid = ambientInterface->getGrid(); + gridDelta = reactionGrid.getGridDelta(); + + if (std::abs(gridDelta - ambientGrid.getGridDelta()) > + std::numeric_limits::epsilon() || + (maskInterface != nullptr && + std::abs(gridDelta - maskInterface->getGrid().getGridDelta()) > + std::numeric_limits::epsilon())) { + Logger::getInstance() + .addError(solverName + ": Interface grid deltas must match.") + .print(); + return false; + } + + IndexType gridMin = reactionGrid.getMinGridPoint(); + IndexType gridMax = reactionGrid.getMaxGridPoint(); + for (unsigned i = 0; i < D; ++i) { + gridMin[i] = std::max(gridMin[i], ambientGrid.getMinGridPoint(i)); + gridMax[i] = std::min(gridMax[i], ambientGrid.getMaxGridPoint(i)); + if (maskInterface != nullptr) { + gridMin[i] = + std::max(gridMin[i], maskInterface->getGrid().getMinGridPoint(i)); + gridMax[i] = + std::min(gridMax[i], maskInterface->getGrid().getMaxGridPoint(i)); + } + } + + auto bounds = definedPointBounds(reactionInterface); + mergeDefinedPointBounds(bounds, ambientInterface); + if (maskInterface != nullptr) + mergeDefinedPointBounds(bounds, maskInterface); + + minIndex = bounds.foundDefinedPoints ? bounds.min : gridMin; + maxIndex = bounds.foundDefinedPoints ? bounds.max : gridMax; + applyBoundsPaddingAndClamp(minIndex, maxIndex, gridMin, gridMax); + + std::size_t numGridPoints = 1; + for (unsigned i = 0; i < D; ++i) { + if (useRequestedBounds) { + minIndex[i] = std::max(minIndex[i], requestedMinIndex[i]); + maxIndex[i] = std::min(maxIndex[i], requestedMaxIndex[i]); + } + if (maxIndex[i] < minIndex[i]) { + Logger::getInstance() + .addError(solverName + ": Cartesian solve region is empty.") + .print(); + return false; + } + extents[i] = static_cast(maxIndex[i] - minIndex[i] + 1); + numGridPoints *= extents[i]; + strides[i] = (i == 0) ? 1 : strides[i - 1] * extents[i - 1]; + } + + if (numGridPoints > maxGridPoints) { + Logger::getInstance() + .addError(solverName + + ": Cartesian solve region exceeds maxGridPoints.") + .print(); + return false; + } + return true; + } + + bool initializeGridFromMask(SmartPointer> maskInterface, + bool useRequestedBounds, + const IndexType &requestedMinIndex, + const IndexType &requestedMaxIndex, + std::size_t maxGridPoints, + const std::string &solverName) { + auto &grid = maskInterface->getGrid(); + gridDelta = grid.getGridDelta(); + + auto bounds = definedPointBounds(maskInterface); + minIndex = bounds.foundDefinedPoints ? bounds.min : grid.getMinGridPoint(); + maxIndex = bounds.foundDefinedPoints ? bounds.max : grid.getMaxGridPoint(); + applyBoundsPaddingAndClamp(minIndex, maxIndex, grid.getMinGridPoint(), + grid.getMaxGridPoint()); + + std::size_t numGridPoints = 1; + for (unsigned i = 0; i < D; ++i) { + if (useRequestedBounds) { + minIndex[i] = std::max(minIndex[i], requestedMinIndex[i]); + maxIndex[i] = std::min(maxIndex[i], requestedMaxIndex[i]); + } + if (maxIndex[i] < minIndex[i]) { + Logger::getInstance() + .addError(solverName + ": Cartesian solve region is empty.") + .print(); + return false; + } + extents[i] = static_cast(maxIndex[i] - minIndex[i] + 1); + numGridPoints *= extents[i]; + strides[i] = (i == 0) ? 1 : strides[i - 1] * extents[i - 1]; + } + + if (numGridPoints > maxGridPoints) { + Logger::getInstance() + .addError(solverName + + ": Cartesian solve region exceeds maxGridPoints.") + .print(); + return false; + } + return true; + } + +private: + GridBounds definedPointBounds(SmartPointer> levelSet) const { + GridBounds bounds; + ConstSparseIterator it(levelSet->getDomain()); + for (; !it.isFinished(); ++it) { + if (!it.isDefined()) + continue; + + const auto &index = it.getStartIndices(); + if (!bounds.foundDefinedPoints) { + bounds.min = index; + bounds.max = index; + bounds.foundDefinedPoints = true; + } else { + for (unsigned i = 0; i < D; ++i) { + bounds.min[i] = std::min(bounds.min[i], index[i]); + bounds.max[i] = std::max(bounds.max[i], index[i]); + } + } + } + return bounds; + } + + void mergeDefinedPointBounds(GridBounds &target, + SmartPointer> levelSet) const { + const auto source = definedPointBounds(levelSet); + if (!source.foundDefinedPoints) + return; + if (!target.foundDefinedPoints) { + target = source; + return; + } + for (unsigned i = 0; i < D; ++i) { + target.min[i] = std::min(target.min[i], source.min[i]); + target.max[i] = std::max(target.max[i], source.max[i]); + } + } + + static void applyBoundsPaddingAndClamp(IndexType &minIndex, + IndexType &maxIndex, + const IndexType &gridMin, + const IndexType &gridMax) { + constexpr int padding = 4; + for (unsigned i = 0; i < D; ++i) { + // Clamp to the physical grid before adding/subtracting padding. + // lsInterior on an INFINITE_BOUNDARY level-set can leave far-field + // sentinel entries (near std::numeric_limits::min/max) in + // the HRLE structure. definedPointBounds() visits them and records + // the extreme index. Subtracting padding from that sentinel directly + // overflows, turning the tiny sentinel into a large positive value and + // making extents[i] enormous. Clamping first makes the arithmetic safe. + minIndex[i] = + std::max(gridMin[i], std::max(gridMin[i], minIndex[i]) - padding); + maxIndex[i] = + std::min(gridMax[i], std::min(gridMax[i], maxIndex[i]) + padding); + } + } +}; + +} // namespace viennals diff --git a/include/viennals/lsVersion.hpp b/include/viennals/lsVersion.hpp index b18da157..2d6b556d 100644 --- a/include/viennals/lsVersion.hpp +++ b/include/viennals/lsVersion.hpp @@ -5,10 +5,10 @@ namespace viennals { // Version information generated by CMake -inline constexpr const char *version = "5.7.3"; +inline constexpr const char *version = "5.8.0"; inline constexpr int versionMajor = 5; -inline constexpr int versionMinor = 7; -inline constexpr int versionPatch = 3; +inline constexpr int versionMinor = 8; +inline constexpr int versionPatch = 0; // Utility functions for version comparison inline constexpr uint32_t versionAsInteger() { diff --git a/pyproject.toml b/pyproject.toml index 0e04f6eb..ff4bfaa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ requires = [ build-backend = "scikit_build_core.build" [project] -version = "5.7.3" +version = "5.8.0" name = "ViennaLS" readme = "README.md" license = {file = "LICENSE"} diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 1b3534f5..5b17c4db 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.14) project(ViennaLS_Python LANGUAGES CXX) +option(VIENNALS_USE_GPU "Enable GPU-accelerated BiCGSTAB linear solver via CUDA" OFF) + add_custom_target(${PROJECT_NAME} ALL) # -------------------------------------------------------------------------------------------------------- @@ -59,6 +61,13 @@ if(VIENNALS_VTK_RENDERING) target_compile_definitions(${MOD} PRIVATE VIENNALS_VTK_MODULE_INIT) endif() +if(VIENNALS_USE_GPU) + message(STATUS "[ViennaLS] Adding GPU support for 'viennals' python package") + if(TARGET ViennaLS_GPU) + add_dependencies(${MOD} ViennaLS_GPU) + endif() +endif() + # Put the built .so in build/ for convenience set_target_properties( ${MOD} @@ -88,6 +97,18 @@ if((VIENNALS_PACKAGE_PYTHON AND NOT APPLE) OR MSVC) # macOS wheels let delocate own dylib vendoring to avoid duplicate VTK copies. # Other package builds still need this staged dependency folder. viennacore_setup_vtk_env(${MOD} ${VIENNALS_LIB_FOLDER}) + + if(VIENNALS_USE_GPU) + foreach(_CUDA_TARGET CUDA::cudart CUDA::cusparse) + if(TARGET ${_CUDA_TARGET}) + add_custom_command( + TARGET ${MOD} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory $ + ${VIENNALS_LIB_FOLDER}) + endif() + endforeach() + endif() endif() install( diff --git a/python/__init__.py b/python/__init__.py index 351206d4..82f33b80 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -37,6 +37,19 @@ def _windows_dll_path(): d3 = _C.d3 _sys.modules[__name__ + ".d2"] = d2 _sys.modules[__name__ + ".d3"] = d3 + +_SHARED_OXIDATION_TYPES = ( + "OxidationParameters", + "OxidationPresets", + "OxidationDeformationParameters", + "OxidationMaskParameters", + "OxidationCouplingParameters", +) +for _name in _SHARED_OXIDATION_TYPES: + if hasattr(_C, _name): + setattr(d2, _name, getattr(_C, _name)) + setattr(d3, _name, getattr(_C, _name)) + PROXY_DIM = 2 # default dimension for proxy classes diff --git a/python/pyWrap.cpp b/python/pyWrap.cpp index 1cb31500..d920729b 100644 --- a/python/pyWrap.cpp +++ b/python/pyWrap.cpp @@ -390,6 +390,8 @@ PYBIND11_MODULE(VIENNALS_MODULE_NAME, module) { "Return the analytical dissipation alpha value if the " "lsLocalLaxFriedrichsAnalytical scheme is used for advection."); + bindOxidationSharedTypes(module); + // ---------- MAIN API ---------- // Submodule for 2D auto m2 = module.def_submodule("d2", "2D bindings"); diff --git a/python/pyWrap.hpp b/python/pyWrap.hpp index 5da4b68f..03fb95b4 100644 --- a/python/pyWrap.hpp +++ b/python/pyWrap.hpp @@ -31,6 +31,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -88,6 +91,160 @@ class PylsGeometricAdvectDistribution } }; +inline void bindOxidationSharedTypes(py::module &module) { + py::enum_(module, "GpuMode", py::module_local()) + .value("Cpu", GpuMode::Cpu) + .value("Gpu", GpuMode::Gpu) + .export_values(); + + py::enum_(module, "GpuPreconditioner", py::module_local()) + .value("Jacobi", GpuPreconditioner::Jacobi) + .value("ILU0", GpuPreconditioner::ILU0) + .export_values(); + + py::class_(module, "OxidationParameters", + py::module_local()) + .def(py::init<>()) + .def_readwrite("diffusionCoefficient", + &OxidationParameters::diffusionCoefficient) + .def_readwrite("reactionRate", &OxidationParameters::reactionRate) + .def_readwrite("transferCoefficient", + &OxidationParameters::transferCoefficient) + .def_readwrite("equilibriumConcentration", + &OxidationParameters::equilibriumConcentration) + .def_readwrite("oxidantMoleculeDensity", + &OxidationParameters::oxidantMoleculeDensity) + .def_readwrite("expansionCoefficient", + &OxidationParameters::expansionCoefficient) + .def_readwrite("velocitySign", &OxidationParameters::velocitySign) + .def_readwrite("temperature", &OxidationParameters::temperature) + .def_readwrite("reactionActivationVolume", + &OxidationParameters::reactionActivationVolume) + .def_readwrite("referencePressure", + &OxidationParameters::referencePressure) + .def_readwrite("diffusionActivationVolume", + &OxidationParameters::diffusionActivationVolume) + .def_readwrite("reactionRateRatio111", + &OxidationParameters::reactionRateRatio111) + .def_readwrite("crystalAxis", &OxidationParameters::crystalAxis) + .def_readwrite("maskTransferCoefficient", + &OxidationParameters::maskTransferCoefficient) + .def_readwrite("maskConcentration", + &OxidationParameters::maskConcentration) + .def_readwrite("minBoundaryDistance", + &OxidationParameters::minBoundaryDistance) + .def_readwrite("maxIterations", &OxidationParameters::maxIterations) + .def_readwrite("tolerance", &OxidationParameters::tolerance) + .def_readwrite("relaxation", &OxidationParameters::relaxation) + .def_readwrite("maxGridPoints", &OxidationParameters::maxGridPoints) + .def_readwrite("material", &OxidationParameters::material); + + py::class_(module, "OxidationPresets", py::module_local()) + .def(py::init<>()) + .def_static("wet1000CDealGrove100", + &OxidationPresets::wet1000CDealGrove100) + .def_static("oxideMechanics1000C", &OxidationPresets::oxideMechanics1000C) + .def_static("siliconNitrideMask1000C", + &OxidationPresets::siliconNitrideMask1000C); + + py::class_( + module, "OxidationDeformationParameters", py::module_local()) + .def(py::init<>()) + .def_readwrite("viscosity", &OxidationDeformationParameters::viscosity) + .def_readwrite("bulkModulus", + &OxidationDeformationParameters::bulkModulus) + .def_readwrite("ambientPressure", + &OxidationDeformationParameters::ambientPressure) + .def_readwrite("pressureRelaxation", + &OxidationDeformationParameters::pressureRelaxation) + .def_readwrite("pressureTolerance", + &OxidationDeformationParameters::pressureTolerance) + .def_readwrite( + "minMechanicsBoundaryDistance", + &OxidationDeformationParameters::minMechanicsBoundaryDistance) + .def_readwrite("shearModulus", + &OxidationDeformationParameters::shearModulus) + .def_readwrite("stressRelaxationTime", + &OxidationDeformationParameters::stressRelaxationTime) + .def_readwrite("stressTimeStep", + &OxidationDeformationParameters::stressTimeStep) + .def_readwrite("harmonicIterations", + &OxidationDeformationParameters::harmonicIterations) + .def_readwrite("mechanicsIterations", + &OxidationDeformationParameters::mechanicsIterations) + .def_readwrite("pressureIterations", + &OxidationDeformationParameters::pressureIterations) + .def_readwrite("stokesIterations", + &OxidationDeformationParameters::stokesIterations) + .def_readwrite("mechanicsTolerance", + &OxidationDeformationParameters::mechanicsTolerance) + .def_readwrite("stokesTolerance", + &OxidationDeformationParameters::stokesTolerance) + .def_readwrite("tolerance", &OxidationDeformationParameters::tolerance) + .def_readwrite("relaxation", &OxidationDeformationParameters::relaxation) + .def_readwrite("maxGridPoints", + &OxidationDeformationParameters::maxGridPoints) + .def_readwrite("material", &OxidationDeformationParameters::material); + + py::class_(module, "OxidationMaskParameters", + py::module_local()) + .def(py::init<>()) + .def_readwrite("contactMode", &OxidationMaskParameters::contactMode) + .def_readwrite("temperature", &OxidationMaskParameters::temperature) + .def_readwrite("referenceTemperature", + &OxidationMaskParameters::referenceTemperature) + .def_readwrite("referenceViscosity", + &OxidationMaskParameters::referenceViscosity) + .def_readwrite("creepActivationEnergy", + &OxidationMaskParameters::creepActivationEnergy) + .def_readwrite("poissonRatio", &OxidationMaskParameters::poissonRatio) + .def_readwrite("youngModulus", &OxidationMaskParameters::youngModulus) + .def_readwrite("unilateralContact", + &OxidationMaskParameters::unilateralContact) + .def_readwrite("relaxation", &OxidationMaskParameters::relaxation) + .def_readwrite("contactLoadRelaxation", + &OxidationMaskParameters::contactLoadRelaxation) + .def_readwrite("contactReleaseFraction", + &OxidationMaskParameters::contactReleaseFraction) + .def_readwrite("multigridSmootherOmega", + &OxidationMaskParameters::multigridSmootherOmega) + .def_readwrite("tolerance", &OxidationMaskParameters::tolerance) + .def_readwrite("minBoundaryDistance", + &OxidationMaskParameters::minBoundaryDistance) + .def_readwrite("maxIterations", &OxidationMaskParameters::maxIterations) + .def_readwrite("maxGridPoints", &OxidationMaskParameters::maxGridPoints) + .def_readwrite("material", &OxidationMaskParameters::material) + .def_readwrite("anchorBoundaryDirection", + &OxidationMaskParameters::anchorBoundaryDirection) + .def_readwrite("anchorBoundarySide", + &OxidationMaskParameters::anchorBoundarySide) + .def_readwrite("anchorBoundaryLayers", + &OxidationMaskParameters::anchorBoundaryLayers); + + py::class_(module, "OxidationCouplingParameters", + py::module_local()) + .def(py::init<>()) + .def_readwrite("maxIterations", + &OxidationCouplingParameters::maxIterations) + .def_readwrite("tolerance", &OxidationCouplingParameters::tolerance) + .def_readwrite("relaxation", &OxidationCouplingParameters::relaxation); + + py::class_>( + module, "LOCOSConservationDiagnostics", py::module_local()) + .def(py::init<>()) + .def_readwrite("siliconRecession", + &LOCOSConservationDiagnostics::siliconRecession) + .def_readwrite("ambientLift", + &LOCOSConservationDiagnostics::ambientLift) + .def_readwrite("expectedAmbientLift", + &LOCOSConservationDiagnostics::expectedAmbientLift) + .def_readwrite("ambientLiftRatio", + &LOCOSConservationDiagnostics::ambientLiftRatio) + .def_readwrite("relativeError", + &LOCOSConservationDiagnostics::relativeError) + .def_readwrite("samples", &LOCOSConservationDiagnostics::samples); +} + template void bindApi(py::module &module) { // Also wrap hrleGrid so it can be used to create new LevelSets py::class_>(module, "hrleGrid"); @@ -258,6 +415,367 @@ template void bindApi(py::module &module) { module.def("FinalizeStencilLocalLaxFriedrichs", &FinalizeStencilLocalLaxFriedrichs, py::arg("levelSets")); + // ReactionBoundarySample β€” sub-grid crossing info returned by + // OxidationDiffusion::getReactionBoundarySample + { + using RBS = typename OxidationDiffusion::ReactionBoundarySample; + py::class_(module, "ReactionBoundarySample") + .def(py::init<>()) + .def_readwrite("found", &RBS::found) + .def_readwrite("distance", &RBS::distance) + .def_readwrite("concentration", &RBS::concentration) + .def_readwrite("crossingAxis", &RBS::crossingAxis) + .def_readwrite("crossingOffset", &RBS::crossingOffset) + .def_property( + "nodeIndex", + [](const RBS &s) { + std::array arr{}; + for (unsigned i = 0; i < D; ++i) + arr[i] = s.nodeIndex[i]; + return arr; + }, + [](RBS &s, const std::array &arr) { + for (unsigned i = 0; i < D; ++i) + s.nodeIndex[i] = arr[i]; + }); + } + + py::class_, VelocityField, + SmartPointer>>(module, + "OxidationDiffusion") + .def(py::init([](SmartPointer> &reactionInterface, + SmartPointer> &ambientInterface, + OxidationParameters parameters) { + return OxidationDiffusion::New(reactionInterface, + ambientInterface, parameters); + }), + py::arg("reactionInterface"), py::arg("ambientInterface"), + py::arg("parameters") = OxidationParameters()) + .def("setReactionInterface", + &OxidationDiffusion::setReactionInterface) + .def("setAmbientInterface", + &OxidationDiffusion::setAmbientInterface) + .def("setMaskInterface", &OxidationDiffusion::setMaskInterface, + py::arg("maskInterface"), py::arg("maskSign") = 1) + .def("clearMaskInterface", &OxidationDiffusion::clearMaskInterface) + .def("setParameters", &OxidationDiffusion::setParameters) + .def("getParameters", &OxidationDiffusion::getParameters) + .def("setOxideSigns", &OxidationDiffusion::setOxideSigns) + .def( + "setSolveBounds", + [](OxidationDiffusion &model, + std::array passedMinIndex, + std::array passedMaxIndex) { + viennahrle::Index minIndex{}; + viennahrle::Index maxIndex{}; + for (unsigned i = 0; i < D; ++i) { + minIndex[i] = passedMinIndex[i]; + maxIndex[i] = passedMaxIndex[i]; + } + model.setSolveBounds(minIndex, maxIndex); + }, + py::arg("minIndex"), py::arg("maxIndex")) + .def("clearSolveBounds", &OxidationDiffusion::clearSolveBounds) + .def("apply", &OxidationDiffusion::apply) + .def("getConcentration", + (T(OxidationDiffusion::*)(const Vec3D &) const) & + OxidationDiffusion::getConcentration) + .def("getEffectiveReactionRate", + (T(OxidationDiffusion::*)(const Vec3D &) const) & + OxidationDiffusion::getEffectiveReactionRate) + .def("clearPressureField", &OxidationDiffusion::clearPressureField) + .def("setPressure", + (void(OxidationDiffusion::*)(const Vec3D &, T)) & + OxidationDiffusion::setPressure) + .def("getIterations", &OxidationDiffusion::getIterations) + .def("getResidual", &OxidationDiffusion::getResidual) + .def("getNumberOfSolutionNodes", + &OxidationDiffusion::getNumberOfSolutionNodes) + .def("getReactionBoundarySample", + &OxidationDiffusion::getReactionBoundarySample) + .def("getScalarVelocityFromSample", + &OxidationDiffusion::getScalarVelocityFromSample) + .def("markSolved", &OxidationDiffusion::markSolved) + .def("markGeometryChanged", + &OxidationDiffusion::markGeometryChanged) + .def("writePersistentFields", + &OxidationDiffusion::writePersistentFields) + .def("getConcentrationCache", + &OxidationDiffusion::getConcentrationCache, + py::return_value_policy::copy) + .def("setConcentrationCache", + &OxidationDiffusion::setConcentrationCache); + + py::class_, VelocityField, + SmartPointer>>(module, + "OxidationDeformation") + .def(py::init([](SmartPointer> &reactionInterface, + SmartPointer> &ambientInterface, + SmartPointer> &diffusionField, + OxidationParameters oxidationParameters, + OxidationDeformationParameters deformationParameters) { + return OxidationDeformation::New( + reactionInterface, ambientInterface, diffusionField, + oxidationParameters, deformationParameters); + }), + py::arg("reactionInterface"), py::arg("ambientInterface"), + py::arg("diffusionField"), py::arg("oxidationParameters"), + py::arg("deformationParameters") = OxidationDeformationParameters()) + .def("setMaskInterface", &OxidationDeformation::setMaskInterface, + py::arg("maskInterface"), py::arg("maskSign") = 1) + .def("clearMaskInterface", + &OxidationDeformation::clearMaskInterface) + .def( + "setSolveBounds", + [](OxidationDeformation &model, + std::array passedMinIndex, + std::array passedMaxIndex) { + viennahrle::Index minIndex{}; + viennahrle::Index maxIndex{}; + for (unsigned i = 0; i < D; ++i) { + minIndex[i] = passedMinIndex[i]; + maxIndex[i] = passedMaxIndex[i]; + } + model.setSolveBounds(minIndex, maxIndex); + }, + py::arg("minIndex"), py::arg("maxIndex")) + .def("clearSolveBounds", &OxidationDeformation::clearSolveBounds) + .def("apply", &OxidationDeformation::apply) + .def("getVelocity", + (Vec3D(OxidationDeformation::*)(const Vec3D &) const) & + OxidationDeformation::getVelocity) + .def("getPressure", + (T(OxidationDeformation::*)(const Vec3D &) const) & + OxidationDeformation::getPressure) + .def("getStrainTrace", + (T(OxidationDeformation::*)(const Vec3D &) const) & + OxidationDeformation::getStrainTrace) + .def("getStrainRateTensor", + (std::array(OxidationDeformation::*)(const Vec3D &) + const) & + OxidationDeformation::getStrainRateTensor) + .def("getStressTensor", (std::array(OxidationDeformation::*)( + const Vec3D &) const) & + OxidationDeformation::getStressTensor) + .def("getVonMisesStress", + (T(OxidationDeformation::*)(const Vec3D &) const) & + OxidationDeformation::getVonMisesStress) + .def("getIterations", &OxidationDeformation::getIterations) + .def("getResidual", &OxidationDeformation::getResidual) + .def("getNumberOfSolutionNodes", + &OxidationDeformation::getNumberOfSolutionNodes) + .def("avgExpansionSpeed", &OxidationDeformation::avgExpansionSpeed) + .def("setAmbientInterface", + &OxidationDeformation::setAmbientInterface) + .def("setOxidationParameters", + &OxidationDeformation::setOxidationParameters) + .def("setDeformationParameters", + &OxidationDeformation::setDeformationParameters) + .def("setOxideSigns", &OxidationDeformation::setOxideSigns) + .def("setReactionInterface", + &OxidationDeformation::setReactionInterface) + .def("setDiffusionField", &OxidationDeformation::setDiffusionField) + .def("setMaskVelocityField", + &OxidationDeformation::setMaskVelocityField) + .def("clearMaskVelocityField", + &OxidationDeformation::clearMaskVelocityField) + .def("markGeometryChanged", + &OxidationDeformation::markGeometryChanged) + .def("writeFieldsToLevelSet", + &OxidationDeformation::writeFieldsToLevelSet); + + py::class_, VelocityField, + SmartPointer>>(module, + "OxidationMaskBending") + .def(py::init( + [](SmartPointer> &deformationField, + OxidationMaskParameters maskParameters) { + return OxidationMaskBending::New(deformationField, + maskParameters); + }), + py::arg("deformationField"), + py::arg("maskParameters") = OxidationMaskParameters()) + .def(py::init( + [](SmartPointer> &deformationField, + SmartPointer> &maskInterface, + OxidationMaskParameters maskParameters, int maskSign) { + return OxidationMaskBending::New( + deformationField, maskInterface, maskParameters, maskSign); + }), + py::arg("deformationField"), py::arg("maskInterface"), + py::arg("maskParameters") = OxidationMaskParameters(), + py::arg("maskSign") = 1) + .def("setMaskInterface", &OxidationMaskBending::setMaskInterface, + py::arg("maskInterface"), py::arg("maskSign") = 1) + .def("setAmbientInterface", + &OxidationMaskBending::setAmbientInterface, + py::arg("ambientInterface"), py::arg("ambientSign") = -1) + .def( + "setSolveBounds", + [](OxidationMaskBending &model, + std::array passedMinIndex, + std::array passedMaxIndex) { + viennahrle::Index minIndex{}; + viennahrle::Index maxIndex{}; + for (unsigned i = 0; i < D; ++i) { + minIndex[i] = passedMinIndex[i]; + maxIndex[i] = passedMaxIndex[i]; + } + model.setSolveBounds(minIndex, maxIndex); + }, + py::arg("minIndex"), py::arg("maxIndex")) + .def("clearSolveBounds", &OxidationMaskBending::clearSolveBounds) + .def("apply", &OxidationMaskBending::apply) + .def("setParameters", &OxidationMaskBending::setParameters) + .def("getParameters", &OxidationMaskBending::getParameters) + .def("getIterations", &OxidationMaskBending::getIterations) + .def("getResidual", &OxidationMaskBending::getResidual) + .def("getLastApplyVelocityChange", + &OxidationMaskBending::getLastApplyVelocityChange) + .def("getNumberOfSolutionNodes", + &OxidationMaskBending::getNumberOfSolutionNodes) + .def("getNumberOfContactNodes", + &OxidationMaskBending::getNumberOfContactNodes) + .def("getNumberOfFixedNodes", + &OxidationMaskBending::getNumberOfFixedNodes) + .def("writeFieldsToLevelSet", + &OxidationMaskBending::writeFieldsToLevelSet); + + py::class_, VelocityField, + SmartPointer>>( + module, "OxidationConstrainedAmbient") + .def(py::init( + [](SmartPointer> &deformationField, + SmartPointer> &maskVelocityField, + SmartPointer> &maskInterface, int maskSign) { + return OxidationConstrainedAmbient::New( + deformationField, maskVelocityField, maskInterface, + maskSign); + }), + py::arg("deformationField"), py::arg("maskVelocityField"), + py::arg("maskInterface"), py::arg("maskSign") = 1); + + py::class_, SmartPointer>>( + module, "OxidationModel") + .def(py::init( + [](SmartPointer> &diffusionField, + SmartPointer> &deformationField, + OxidationCouplingParameters couplingParameters) { + return OxidationModel::New( + diffusionField, deformationField, couplingParameters); + }), + py::arg("diffusionField"), py::arg("deformationField"), + py::arg("couplingParameters") = OxidationCouplingParameters()) + .def("setDiffusionField", &OxidationModel::setDiffusionField) + .def("setDeformationField", &OxidationModel::setDeformationField) + .def("setParameters", &OxidationModel::setParameters) + .def( + "setSolveBounds", + [](OxidationModel &model, + std::array passedMinIndex, + std::array passedMaxIndex) { + viennahrle::Index minIndex{}; + viennahrle::Index maxIndex{}; + for (unsigned i = 0; i < D; ++i) { + minIndex[i] = passedMinIndex[i]; + maxIndex[i] = passedMaxIndex[i]; + } + model.setSolveBounds(minIndex, maxIndex); + }, + py::arg("minIndex"), py::arg("maxIndex")) + .def("clearSolveBounds", &OxidationModel::clearSolveBounds) + .def("apply", &OxidationModel::apply) + .def("getIterations", &OxidationModel::getIterations) + .def("getResidual", &OxidationModel::getResidual); + + py::class_, SmartPointer>>(module, + "Oxidation") + .def(py::init([](SmartPointer> &siInterface, + SmartPointer> &ambientInterface, + SmartPointer> &maskInterface) { + return Oxidation::New(siInterface, ambientInterface, + maskInterface); + }), + py::arg("siInterface"), py::arg("ambientInterface"), + py::arg("maskInterface")) + .def("setSiInterface", &Oxidation::setSiInterface) + .def("setAmbientInterface", &Oxidation::setAmbientInterface) + .def("setMaskInterface", &Oxidation::setMaskInterface) + .def("setOxidationParameters", &Oxidation::setOxidationParameters) + .def("setDeformationParameters", + &Oxidation::setDeformationParameters) + .def("setCouplingParameters", &Oxidation::setCouplingParameters) + .def("setMaskParameters", &Oxidation::setMaskParameters) + .def("setSpatialScheme", &Oxidation::setSpatialScheme) + .def("setTemporalScheme", &Oxidation::setTemporalScheme) + .def("setGpuMode", &Oxidation::setGpuMode, py::arg("mode")) + .def("setGpuPreconditioner", &Oxidation::setGpuPreconditioner, + py::arg("preconditioner")) + .def("setMaskCouplingIterations", + &Oxidation::setMaskCouplingIterations) + .def("setMaskCouplingTolerance", + &Oxidation::setMaskCouplingTolerance) + .def( + "setSolveBounds", + [](Oxidation &model, + std::array passedMinIndex, + std::array passedMaxIndex) { + viennahrle::Index minIndex{}; + viennahrle::Index maxIndex{}; + for (unsigned i = 0; i < D; ++i) { + minIndex[i] = passedMinIndex[i]; + maxIndex[i] = passedMaxIndex[i]; + } + model.setSolveBounds(minIndex, maxIndex); + }, + py::arg("minIndex"), py::arg("maxIndex")) + .def( + "setMaskBendingBounds", + [](Oxidation &model, + std::array passedMinIndex, + std::array passedMaxIndex) { + viennahrle::Index minIndex{}; + viennahrle::Index maxIndex{}; + for (unsigned i = 0; i < D; ++i) { + minIndex[i] = passedMinIndex[i]; + maxIndex[i] = passedMaxIndex[i]; + } + model.setMaskBendingBounds(minIndex, maxIndex); + }, + py::arg("minIndex"), py::arg("maxIndex")) + .def("apply", &Oxidation::apply, py::arg("advectionTime")) + .def("applyCFLLimited", &Oxidation::applyCFLLimited, + py::arg("requestedTime"), py::arg("cflFactor"), + "Execute one CFL-limited LOCOS step; returns the actual time " + "advanced.") + .def("getLastMaxVelocity", &Oxidation::getLastMaxVelocity, + "Maximum interface velocity (Β΅m/hr) from the most recent CFL step.") + .def("getDiffusionField", &Oxidation::getDiffusionField) + .def("getDeformationField", &Oxidation::getDeformationField) + .def("getMaskBendingField", &Oxidation::getMaskBendingField) + .def("getMaskCouplingIterations", + &Oxidation::getMaskCouplingIterations) + .def("getMaskCouplingResidual", + &Oxidation::getMaskCouplingResidual); + + if constexpr (D == 2) { + module.def( + "computeLOCOSOpenWindowConservation", + [](SmartPointer> siInitial, + SmartPointer> siAfter, + SmartPointer> ambientInitial, + SmartPointer> ambientAfter, T xMin, T xMax, + viennahrle::IndexType jMin, viennahrle::IndexType jMax, + T expansionCoefficient) { + return computeLOCOSOpenWindowConservation( + siInitial, siAfter, ambientInitial, ambientAfter, xMin, xMax, + jMin, jMax, expansionCoefficient); + }, + py::arg("siInitial"), py::arg("siAfter"), py::arg("ambientInitial"), + py::arg("ambientAfter"), py::arg("xMin"), py::arg("xMax"), + py::arg("jMin"), py::arg("jMax"), py::arg("expansionCoefficient")); + } + // BooleanOperation py::class_, SmartPointer>>( module, "BooleanOperation") diff --git a/python/scripts/install_ViennaLS.py b/python/scripts/install_ViennaLS.py new file mode 100644 index 00000000..a86d0168 --- /dev/null +++ b/python/scripts/install_ViennaLS.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +""" +Build and install ViennaLS with GPU-accelerated BiCGSTAB support. + +The GPU solver requires CUDA 12+ and a compatible GCC version on Linux. +A virtual environment is created (or reused) and ViennaLS is installed into it. +The script auto-detects the ViennaLS source directory from its own location +(ViennaLS/python/scripts/install_ViennaLS.py β†’ repo root is ../../). + +Usage: + python python/scripts/install_ViennaLS.py # GPU build (default) + python python/scripts/install_ViennaLS.py --no-gpu # CPU-only build +""" + +import argparse +import os +import platform +import re +import shutil +import subprocess +import sys +from pathlib import Path + +REQUIRED_NVCC_MAJOR = 12 + +IS_WINDOWS = sys.platform == "win32" or os.name == "nt" +IS_LINUX = sys.platform.startswith("linux") +OS_NAME = platform.system() + +# Filled in by ensure_cuda() on Linux. +REQUIRED_GCC: list[str] | None = None + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def run(cmd, **kwargs): + print("+", " ".join(str(c) for c in cmd)) + return subprocess.run(cmd, check=True, **kwargs) + + +def run_capture(cmd, **kwargs): + print("+", " ".join(str(c) for c in cmd)) + return subprocess.run( + cmd, check=True, stdout=subprocess.PIPE, text=True, **kwargs + ).stdout.strip() + + +def which_or_fail(name: str) -> str: + p = shutil.which(name) + if not p: + sys.exit(f"ERROR: '{name}' is required but was not found in PATH.") + return p + + +# ── CUDA / compiler checks ──────────────────────────────────────────────────── + +def parse_nvcc_version(): + out = run_capture(["nvcc", "--version"]) + for line in out.splitlines(): + if "release" in line: + part = line.split("release", 1)[1].strip().split(",")[0].strip() + try: + parts = part.split(".") + return int(parts[0]), int(parts[1]) if len(parts) > 1 else 0, part + except Exception: + break + sys.exit("Could not parse nvcc version.") + + +def determine_required_gcc(nvcc_major: int, nvcc_minor: int) -> list[str] | None: + v = nvcc_major * 10 + nvcc_minor + if v < 124: + return ["11", "12"] + if v < 128: + return ["11", "12", "13"] + return None # any GCC is fine + + +def get_default_gcc_version() -> tuple[int, int] | None: + if not shutil.which("gcc"): + return None + try: + out = run_capture(["gcc", "--version"]) + m = re.search(r"(\d+)\.(\d+)", out.splitlines()[0]) + if m: + return int(m.group(1)), int(m.group(2)) + except Exception: + pass + return None + + +def ensure_cuda(): + global REQUIRED_GCC + which_or_fail("nvcc") + major, minor, full = parse_nvcc_version() + if major < REQUIRED_NVCC_MAJOR: + sys.exit(f"CUDA {REQUIRED_NVCC_MAJOR}.0+ required (found {full}).") + print(f"CUDA toolkit: {full}") + if IS_LINUX: + REQUIRED_GCC = determine_required_gcc(major, minor) + if REQUIRED_GCC: + print(f"CUDA {full} requires GCC: {', '.join(REQUIRED_GCC)}") + else: + print(f"CUDA {full} works with any GCC version") + + +def ensure_compilers(): + if IS_WINDOWS: + cl = shutil.which("cl") + if not cl: + print( + "WARNING: 'cl.exe' not found. " + "Run from a Visual Studio Developer Command Prompt." + ) + else: + print(f"Found MSVC: {cl}") + return + + global REQUIRED_GCC + if REQUIRED_GCC is None: + which_or_fail("gcc") + which_or_fail("g++") + ver = get_default_gcc_version() + print(f"Using default GCC {ver[0]}.{ver[1]}" if ver else "Using default GCC") + else: + found = None + for v in REQUIRED_GCC: + if shutil.which(f"gcc-{v}") and shutil.which(f"g++-{v}"): + found = v + break + if found is None: + default = get_default_gcc_version() + if default and str(default[0]) in REQUIRED_GCC: + found = str(default[0]) + print(f"Using default GCC-{found} (compatible with CUDA)") + else: + sys.exit( + f"ERROR: None of the required GCC versions {REQUIRED_GCC} found.\n" + "Please install one of: " + + ", ".join(f"gcc-{v}/g++-{v}" for v in REQUIRED_GCC) + ) + else: + print(f"Using GCC-{found}") + REQUIRED_GCC = found + + +# ── venv ───────────────────────────────────────────────────────────────────── + +def venv_paths(venv_dir: Path): + bindir = "Scripts" if IS_WINDOWS else "bin" + python = venv_dir / bindir / ("python.exe" if IS_WINDOWS else "python") + pip = venv_dir / bindir / ("pip.exe" if IS_WINDOWS else "pip") + return python, pip + + +def create_or_reuse_venv(venv_dir: Path): + _, pip = venv_paths(venv_dir) + if venv_dir.exists() and pip.exists(): + print(f"Reusing existing venv at {venv_dir}") + else: + print(f"Creating venv at {venv_dir}") + run([sys.executable, "-m", "venv", str(venv_dir)]) + + +# ── ViennaLS install ────────────────────────────────────────────────────────── + +def get_viennals_dir() -> Path: + # Script lives at ViennaLS/python/scripts/install_ViennaLS.py, + # so ../.. from here is always the repo root. + candidate = Path(__file__).resolve().parent.parent.parent + if (candidate / "CMakeLists.txt").exists(): + return candidate + # Fallback: current working directory if it looks like the repo root. + cwd = Path.cwd() + if (cwd / "CMakeLists.txt").exists(): + return cwd + sys.exit( + "Could not locate the ViennaLS source directory.\n" + "Run this script from inside the ViennaLS repository." + ) + + +def install_viennals(pip_path: Path, viennals_dir: Path, gpu: bool, + debug: bool, verbose: bool): + if not viennals_dir.exists() or not (viennals_dir / "CMakeLists.txt").exists(): + sys.exit(f"Not a valid ViennaLS directory: {viennals_dir}") + + env = os.environ.copy() + if IS_LINUX and REQUIRED_GCC is not None: + env["CC"] = f"gcc-{REQUIRED_GCC}" + env["CXX"] = f"g++-{REQUIRED_GCC}" + + cmake_args = ["-DVIENNALS_USE_GPU=ON" if gpu else "-DVIENNALS_USE_GPU=OFF"] + env["CMAKE_ARGS"] = " ".join(cmake_args) + + cmd = [str(pip_path), "install", "--no-deps", "."] + if debug: + cmd += ["--config-settings=cmake.build-type=Debug"] + if verbose: + cmd += ["-v"] + + run(cmd, cwd=viennals_dir, env=env) + + +# ── main ───────────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser( + description=f"Install ViennaLS with GPU BiCGSTAB support ({OS_NAME})." + ) + parser.add_argument("--verbose", "-v", action="store_true", + help="Verbose pip build output.") + parser.add_argument("--venv", + default=os.environ.get("VIRTUAL_ENV", ".venv"), + help="Path to virtual environment (default: .venv).") + parser.add_argument("--debug-build", action="store_true", + help="Build in Debug mode.") + parser.add_argument("--no-gpu", action="store_true", + help="Disable GPU support (CPU-only build).") + parser.add_argument("--skip-toolchain-check", action="store_true", + help="Skip CUDA/compiler checks.") + args = parser.parse_args() + + gpu = not args.no_gpu + if gpu and not args.skip_toolchain_check: + print("Checking toolchain...") + ensure_cuda() + ensure_compilers() + + venv_dir = Path(args.venv).expanduser().resolve() + create_or_reuse_venv(venv_dir) + _, venv_pip = venv_paths(venv_dir) + + install_viennals(venv_pip, get_viennals_dir(), gpu, args.debug_build, args.verbose) + + bindir = "Scripts" if IS_WINDOWS else "bin" + activate = venv_dir / bindir / ("activate.bat" if IS_WINDOWS else "activate") + print("\nInstallation complete.") + if IS_WINDOWS: + print(f"Activate venv:\n {activate}") + else: + print(f"Activate venv:\n source {activate}") + print("Deactivate:\n deactivate") + + +if __name__ == "__main__": + main() diff --git a/python/viennals/__init__.pyi b/python/viennals/__init__.pyi index df68a1ca..779995fd 100644 --- a/python/viennals/__init__.pyi +++ b/python/viennals/__init__.pyi @@ -16,11 +16,19 @@ from viennals._core import CurvatureEnum from viennals._core import Extrude from viennals._core import FeatureDetectionEnum from viennals._core import FileFormatEnum +from viennals._core import GpuMode +from viennals._core import GpuPreconditioner +from viennals._core import LOCOSConservationDiagnostics from viennals._core import LogLevel from viennals._core import Logger from viennals._core import MaterialMap from viennals._core import Mesh from viennals._core import NormalCalculationMethodEnum +from viennals._core import OxidationCouplingParameters +from viennals._core import OxidationDeformationParameters +from viennals._core import OxidationMaskParameters +from viennals._core import OxidationParameters +from viennals._core import OxidationPresets from viennals._core import PointData from viennals._core import Slice from viennals._core import SpatialSchemeEnum @@ -62,10 +70,17 @@ from viennals.d2 import GeometricAdvect from viennals.d2 import GeometricAdvectDistribution from viennals.d2 import MakeGeometry from viennals.d2 import MarkVoidPoints +from viennals.d2 import Oxidation +from viennals.d2 import OxidationConstrainedAmbient +from viennals.d2 import OxidationDeformation +from viennals.d2 import OxidationDiffusion +from viennals.d2 import OxidationMaskBending +from viennals.d2 import OxidationModel from viennals.d2 import Plane from viennals.d2 import PointCloud from viennals.d2 import PrepareStencilLocalLaxFriedrichs from viennals.d2 import Prune +from viennals.d2 import ReactionBoundarySample from viennals.d2 import Reader from viennals.d2 import Reduce from viennals.d2 import RemoveStrayPoints @@ -80,11 +95,12 @@ from viennals.d2 import ToSurfaceMesh from viennals.d2 import ToVoxelMesh from viennals.d2 import WriteVisualizationMesh from viennals.d2 import Writer +from viennals.d2 import computeLOCOSOpenWindowConservation from viennals.d2 import hrleGrid from . import _core from . import d2 from . import d3 -__all__: list[str] = ['Advect', 'BooleanOperation', 'BooleanOperationEnum', 'BoundaryConditionEnum', 'Box', 'BoxDistribution', 'CalculateCurvatures', 'CalculateNormalVectors', 'CalculateVisibilities', 'Check', 'CompareArea', 'CompareChamfer', 'CompareCriticalDimensions', 'CompareNarrowBand', 'CompareSparseField', 'CompareVolume', 'ConvexHull', 'CurvatureEnum', 'CustomSphereDistribution', 'Cylinder', 'DetectFeatures', 'Domain', 'Expand', 'Extrude', 'FeatureDetectionEnum', 'FileFormatEnum', 'FinalizeStencilLocalLaxFriedrichs', 'FromMesh', 'FromSurfaceMesh', 'FromVolumeMesh', 'GeometricAdvect', 'GeometricAdvectDistribution', 'IntegrationSchemeEnum', 'LogLevel', 'Logger', 'MakeGeometry', 'MarkVoidPoints', 'MaterialMap', 'Mesh', 'NormalCalculationMethodEnum', 'PROXY_DIM', 'Plane', 'PointCloud', 'PointData', 'PrepareStencilLocalLaxFriedrichs', 'Prune', 'Reader', 'Reduce', 'RemoveStrayPoints', 'Slice', 'SpatialSchemeEnum', 'Sphere', 'SphereDistribution', 'StencilLocalLaxFriedrichsScalar', 'TemporalSchemeEnum', 'ToDiskMesh', 'ToHullMesh', 'ToMesh', 'ToMultiSurfaceMesh', 'ToSurfaceMesh', 'ToVoxelMesh', 'TransformEnum', 'TransformMesh', 'VTKReader', 'VTKRenderWindow', 'VTKWriter', 'VelocityField', 'VoidTopSurfaceEnum', 'WriteVisualizationMesh', 'Writer', 'd2', 'd3', 'getDimension', 'hrleGrid', 'setDimension', 'setNumThreads', 'version'] +__all__: list[str] = ['Advect', 'BooleanOperation', 'BooleanOperationEnum', 'BoundaryConditionEnum', 'Box', 'BoxDistribution', 'CalculateCurvatures', 'CalculateNormalVectors', 'CalculateVisibilities', 'Check', 'CompareArea', 'CompareChamfer', 'CompareCriticalDimensions', 'CompareNarrowBand', 'CompareSparseField', 'CompareVolume', 'ConvexHull', 'Cpu', 'CurvatureEnum', 'CustomSphereDistribution', 'Cylinder', 'DetectFeatures', 'Domain', 'Expand', 'Extrude', 'FeatureDetectionEnum', 'FileFormatEnum', 'FinalizeStencilLocalLaxFriedrichs', 'FromMesh', 'FromSurfaceMesh', 'FromVolumeMesh', 'GeometricAdvect', 'GeometricAdvectDistribution', 'Gpu', 'GpuMode', 'GpuPreconditioner', 'ILU0', 'IntegrationSchemeEnum', 'Jacobi', 'LOCOSConservationDiagnostics', 'LogLevel', 'Logger', 'MakeGeometry', 'MarkVoidPoints', 'MaterialMap', 'Mesh', 'NormalCalculationMethodEnum', 'Oxidation', 'OxidationConstrainedAmbient', 'OxidationCouplingParameters', 'OxidationDeformation', 'OxidationDeformationParameters', 'OxidationDiffusion', 'OxidationMaskBending', 'OxidationMaskParameters', 'OxidationModel', 'OxidationParameters', 'OxidationPresets', 'PROXY_DIM', 'Plane', 'PointCloud', 'PointData', 'PrepareStencilLocalLaxFriedrichs', 'Prune', 'ReactionBoundarySample', 'Reader', 'Reduce', 'RemoveStrayPoints', 'Slice', 'SpatialSchemeEnum', 'Sphere', 'SphereDistribution', 'StencilLocalLaxFriedrichsScalar', 'TemporalSchemeEnum', 'ToDiskMesh', 'ToHullMesh', 'ToMesh', 'ToMultiSurfaceMesh', 'ToSurfaceMesh', 'ToVoxelMesh', 'TransformEnum', 'TransformMesh', 'VTKReader', 'VTKRenderWindow', 'VTKWriter', 'VelocityField', 'VoidTopSurfaceEnum', 'WriteVisualizationMesh', 'Writer', 'computeLOCOSOpenWindowConservation', 'd2', 'd3', 'getDimension', 'hrleGrid', 'setDimension', 'setNumThreads', 'version'] def __dir__(): ... def __getattr__(name): @@ -111,7 +127,13 @@ def setDimension(d: int): Dimension of the simulation (2 or 3). """ +Cpu: _core.GpuMode # value = +Gpu: _core.GpuMode # value = +ILU0: _core.GpuPreconditioner # value = +Jacobi: _core.GpuPreconditioner # value = PROXY_DIM: int = 2 -__version__: str = '5.7.3' -version: str = '5.7.3' +_SHARED_OXIDATION_TYPES: tuple = ('OxidationParameters', 'OxidationPresets', 'OxidationDeformationParameters', 'OxidationMaskParameters', 'OxidationCouplingParameters') +__version__: str = '5.8.0' +_name: str = 'OxidationCouplingParameters' +version: str = '5.8.0' _C = _core diff --git a/python/viennals/_core.pyi b/python/viennals/_core.pyi index 8013e046..663384a8 100644 --- a/python/viennals/_core.pyi +++ b/python/viennals/_core.pyi @@ -7,9 +7,9 @@ import enum import typing from viennals import d2 import viennals.d2 -import viennals.d3 from viennals import d3 -__all__: list[str] = ['BooleanOperationEnum', 'BoundaryConditionEnum', 'CurvatureEnum', 'Extrude', 'FeatureDetectionEnum', 'FileFormatEnum', 'IntegrationSchemeEnum', 'LogLevel', 'Logger', 'MaterialMap', 'Mesh', 'NormalCalculationMethodEnum', 'PointData', 'Slice', 'SpatialSchemeEnum', 'TemporalSchemeEnum', 'TransformEnum', 'TransformMesh', 'VTKReader', 'VTKRenderWindow', 'VTKWriter', 'VelocityField', 'VoidTopSurfaceEnum', 'd2', 'd3', 'setNumThreads', 'version'] +import viennals.d3 +__all__: list[str] = ['BooleanOperationEnum', 'BoundaryConditionEnum', 'Cpu', 'CurvatureEnum', 'Extrude', 'FeatureDetectionEnum', 'FileFormatEnum', 'Gpu', 'GpuMode', 'GpuPreconditioner', 'ILU0', 'IntegrationSchemeEnum', 'Jacobi', 'LOCOSConservationDiagnostics', 'LogLevel', 'Logger', 'MaterialMap', 'Mesh', 'NormalCalculationMethodEnum', 'OxidationCouplingParameters', 'OxidationDeformationParameters', 'OxidationMaskParameters', 'OxidationParameters', 'OxidationPresets', 'PointData', 'Slice', 'SpatialSchemeEnum', 'TemporalSchemeEnum', 'TransformEnum', 'TransformMesh', 'VTKReader', 'VTKRenderWindow', 'VTKWriter', 'VelocityField', 'VoidTopSurfaceEnum', 'd2', 'd3', 'setNumThreads', 'version'] class BooleanOperationEnum(enum.IntEnum): INTERSECT: typing.ClassVar[BooleanOperationEnum] # value = INVERT: typing.ClassVar[BooleanOperationEnum] # value = @@ -104,6 +104,119 @@ class FileFormatEnum(enum.IntEnum): """ Convert to a string according to format_spec. """ +class GpuMode: + """ + Members: + + Cpu + + Gpu + """ + Cpu: typing.ClassVar[GpuMode] # value = + Gpu: typing.ClassVar[GpuMode] # value = + __members__: typing.ClassVar[dict[str, GpuMode]] # value = {'Cpu': , 'Gpu': } + def __eq__(self, other: typing.Any) -> bool: + ... + def __getstate__(self) -> int: + ... + def __hash__(self) -> int: + ... + def __index__(self) -> int: + ... + def __init__(self, value: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def __int__(self) -> int: + ... + def __ne__(self, other: typing.Any) -> bool: + ... + def __repr__(self) -> str: + ... + def __setstate__(self, state: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def __str__(self) -> str: + ... + @property + def name(self) -> str: + ... + @property + def value(self) -> int: + ... +class GpuPreconditioner: + """ + Members: + + Jacobi + + ILU0 + """ + ILU0: typing.ClassVar[GpuPreconditioner] # value = + Jacobi: typing.ClassVar[GpuPreconditioner] # value = + __members__: typing.ClassVar[dict[str, GpuPreconditioner]] # value = {'Jacobi': , 'ILU0': } + def __eq__(self, other: typing.Any) -> bool: + ... + def __getstate__(self) -> int: + ... + def __hash__(self) -> int: + ... + def __index__(self) -> int: + ... + def __init__(self, value: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def __int__(self) -> int: + ... + def __ne__(self, other: typing.Any) -> bool: + ... + def __repr__(self) -> str: + ... + def __setstate__(self, state: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def __str__(self) -> str: + ... + @property + def name(self) -> str: + ... + @property + def value(self) -> int: + ... +class LOCOSConservationDiagnostics: + def __init__(self) -> None: + ... + @property + def ambientLift(self) -> float: + ... + @ambientLift.setter + def ambientLift(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def ambientLiftRatio(self) -> float: + ... + @ambientLiftRatio.setter + def ambientLiftRatio(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def expectedAmbientLift(self) -> float: + ... + @expectedAmbientLift.setter + def expectedAmbientLift(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def relativeError(self) -> float: + ... + @relativeError.setter + def relativeError(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def samples(self) -> int: + ... + @samples.setter + def samples(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def siliconRecession(self) -> float: + ... + @siliconRecession.setter + def siliconRecession(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... class LogLevel: """ Members: @@ -306,6 +419,403 @@ class NormalCalculationMethodEnum(enum.IntEnum): """ Convert to a string according to format_spec. """ +class OxidationCouplingParameters: + def __init__(self) -> None: + ... + @property + def maxIterations(self) -> int: + ... + @maxIterations.setter + def maxIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def relaxation(self) -> float: + ... + @relaxation.setter + def relaxation(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def tolerance(self) -> float: + ... + @tolerance.setter + def tolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... +class OxidationDeformationParameters: + def __init__(self) -> None: + ... + @property + def ambientPressure(self) -> float: + ... + @ambientPressure.setter + def ambientPressure(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def bulkModulus(self) -> float: + ... + @bulkModulus.setter + def bulkModulus(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def harmonicIterations(self) -> int: + ... + @harmonicIterations.setter + def harmonicIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def material(self) -> int: + ... + @material.setter + def material(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def maxGridPoints(self) -> int: + ... + @maxGridPoints.setter + def maxGridPoints(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def mechanicsIterations(self) -> int: + ... + @mechanicsIterations.setter + def mechanicsIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def mechanicsTolerance(self) -> float: + ... + @mechanicsTolerance.setter + def mechanicsTolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def minMechanicsBoundaryDistance(self) -> float: + ... + @minMechanicsBoundaryDistance.setter + def minMechanicsBoundaryDistance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def pressureIterations(self) -> int: + ... + @pressureIterations.setter + def pressureIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def pressureRelaxation(self) -> float: + ... + @pressureRelaxation.setter + def pressureRelaxation(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def pressureTolerance(self) -> float: + ... + @pressureTolerance.setter + def pressureTolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def relaxation(self) -> float: + ... + @relaxation.setter + def relaxation(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def shearModulus(self) -> float: + ... + @shearModulus.setter + def shearModulus(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def stokesIterations(self) -> int: + ... + @stokesIterations.setter + def stokesIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def stokesTolerance(self) -> float: + ... + @stokesTolerance.setter + def stokesTolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def stressRelaxationTime(self) -> float: + ... + @stressRelaxationTime.setter + def stressRelaxationTime(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def stressTimeStep(self) -> float: + ... + @stressTimeStep.setter + def stressTimeStep(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def tolerance(self) -> float: + ... + @tolerance.setter + def tolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def viscosity(self) -> float: + ... + @viscosity.setter + def viscosity(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... +class OxidationMaskParameters: + unilateralContact: bool + def __init__(self) -> None: + ... + @property + def anchorBoundaryDirection(self) -> int: + ... + @anchorBoundaryDirection.setter + def anchorBoundaryDirection(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def anchorBoundaryLayers(self) -> int: + ... + @anchorBoundaryLayers.setter + def anchorBoundaryLayers(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def anchorBoundarySide(self) -> int: + ... + @anchorBoundarySide.setter + def anchorBoundarySide(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def contactLoadRelaxation(self) -> float: + ... + @contactLoadRelaxation.setter + def contactLoadRelaxation(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def contactMode(self) -> int: + ... + @contactMode.setter + def contactMode(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def contactReleaseFraction(self) -> float: + ... + @contactReleaseFraction.setter + def contactReleaseFraction(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def creepActivationEnergy(self) -> float: + ... + @creepActivationEnergy.setter + def creepActivationEnergy(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def material(self) -> int: + ... + @material.setter + def material(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def maxGridPoints(self) -> int: + ... + @maxGridPoints.setter + def maxGridPoints(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def maxIterations(self) -> int: + ... + @maxIterations.setter + def maxIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def minBoundaryDistance(self) -> float: + ... + @minBoundaryDistance.setter + def minBoundaryDistance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def multigridSmootherOmega(self) -> float: + ... + @multigridSmootherOmega.setter + def multigridSmootherOmega(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def poissonRatio(self) -> float: + ... + @poissonRatio.setter + def poissonRatio(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def referenceTemperature(self) -> float: + ... + @referenceTemperature.setter + def referenceTemperature(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def referenceViscosity(self) -> float: + ... + @referenceViscosity.setter + def referenceViscosity(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def relaxation(self) -> float: + ... + @relaxation.setter + def relaxation(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def temperature(self) -> float: + ... + @temperature.setter + def temperature(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def tolerance(self) -> float: + ... + @tolerance.setter + def tolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def youngModulus(self) -> float: + ... + @youngModulus.setter + def youngModulus(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... +class OxidationParameters: + def __init__(self) -> None: + ... + @property + def crystalAxis(self) -> typing.Annotated[list[float], "FixedSize(3)"]: + ... + @crystalAxis.setter + def crystalAxis(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> None: + ... + @property + def diffusionActivationVolume(self) -> float: + ... + @diffusionActivationVolume.setter + def diffusionActivationVolume(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def diffusionCoefficient(self) -> float: + ... + @diffusionCoefficient.setter + def diffusionCoefficient(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def equilibriumConcentration(self) -> float: + ... + @equilibriumConcentration.setter + def equilibriumConcentration(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def expansionCoefficient(self) -> float: + ... + @expansionCoefficient.setter + def expansionCoefficient(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def maskConcentration(self) -> float: + ... + @maskConcentration.setter + def maskConcentration(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def maskTransferCoefficient(self) -> float: + ... + @maskTransferCoefficient.setter + def maskTransferCoefficient(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def material(self) -> int: + ... + @material.setter + def material(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def maxGridPoints(self) -> int: + ... + @maxGridPoints.setter + def maxGridPoints(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def maxIterations(self) -> int: + ... + @maxIterations.setter + def maxIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def minBoundaryDistance(self) -> float: + ... + @minBoundaryDistance.setter + def minBoundaryDistance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def oxidantMoleculeDensity(self) -> float: + ... + @oxidantMoleculeDensity.setter + def oxidantMoleculeDensity(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def reactionActivationVolume(self) -> float: + ... + @reactionActivationVolume.setter + def reactionActivationVolume(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def reactionRate(self) -> float: + ... + @reactionRate.setter + def reactionRate(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def reactionRateRatio111(self) -> float: + ... + @reactionRateRatio111.setter + def reactionRateRatio111(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def referencePressure(self) -> float: + ... + @referencePressure.setter + def referencePressure(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def relaxation(self) -> float: + ... + @relaxation.setter + def relaxation(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def temperature(self) -> float: + ... + @temperature.setter + def temperature(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def tolerance(self) -> float: + ... + @tolerance.setter + def tolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def transferCoefficient(self) -> float: + ... + @transferCoefficient.setter + def transferCoefficient(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def velocitySign(self) -> float: + ... + @velocitySign.setter + def velocitySign(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... +class OxidationPresets: + @staticmethod + def oxideMechanics1000C(arg0: typing.SupportsFloat | typing.SupportsIndex) -> ...: + ... + @staticmethod + def siliconNitrideMask1000C() -> ...: + ... + @staticmethod + def wet1000CDealGrove100() -> OxidationParameters: + ... + def __init__(self) -> None: + ... class PointData: def __init__(self) -> None: ... @@ -549,6 +1059,10 @@ class VoidTopSurfaceEnum(enum.IntEnum): """ def setNumThreads(arg0: typing.SupportsInt | typing.SupportsIndex) -> None: ... -__version__: str = '5.7.3' -version: str = '5.7.3' +Cpu: GpuMode # value = +Gpu: GpuMode # value = +ILU0: GpuPreconditioner # value = +Jacobi: GpuPreconditioner # value = +__version__: str = '5.8.0' +version: str = '5.8.0' IntegrationSchemeEnum = SpatialSchemeEnum diff --git a/python/viennals/d2.pyi b/python/viennals/d2.pyi index 98546335..d7047e01 100644 --- a/python/viennals/d2.pyi +++ b/python/viennals/d2.pyi @@ -5,7 +5,12 @@ from __future__ import annotations import collections.abc import typing import viennals._core -__all__: list[str] = ['Advect', 'BooleanOperation', 'Box', 'BoxDistribution', 'CalculateCurvatures', 'CalculateNormalVectors', 'CalculateVisibilities', 'Check', 'CompareArea', 'CompareChamfer', 'CompareCriticalDimensions', 'CompareNarrowBand', 'CompareSparseField', 'CompareVolume', 'ConvexHull', 'CustomSphereDistribution', 'Cylinder', 'DetectFeatures', 'Domain', 'Expand', 'FinalizeStencilLocalLaxFriedrichs', 'FromMesh', 'FromSurfaceMesh', 'FromVolumeMesh', 'GeometricAdvect', 'GeometricAdvectDistribution', 'MakeGeometry', 'MarkVoidPoints', 'Plane', 'PointCloud', 'PrepareStencilLocalLaxFriedrichs', 'Prune', 'Reader', 'Reduce', 'RemoveStrayPoints', 'Sphere', 'SphereDistribution', 'StencilLocalLaxFriedrichsScalar', 'ToDiskMesh', 'ToHullMesh', 'ToMesh', 'ToMultiSurfaceMesh', 'ToSurfaceMesh', 'ToVoxelMesh', 'WriteVisualizationMesh', 'Writer', 'hrleGrid'] +from viennals._core import OxidationCouplingParameters +from viennals._core import OxidationDeformationParameters +from viennals._core import OxidationMaskParameters +from viennals._core import OxidationParameters +from viennals._core import OxidationPresets +__all__: list[str] = ['Advect', 'BooleanOperation', 'Box', 'BoxDistribution', 'CalculateCurvatures', 'CalculateNormalVectors', 'CalculateVisibilities', 'Check', 'CompareArea', 'CompareChamfer', 'CompareCriticalDimensions', 'CompareNarrowBand', 'CompareSparseField', 'CompareVolume', 'ConvexHull', 'CustomSphereDistribution', 'Cylinder', 'DetectFeatures', 'Domain', 'Expand', 'FinalizeStencilLocalLaxFriedrichs', 'FromMesh', 'FromSurfaceMesh', 'FromVolumeMesh', 'GeometricAdvect', 'GeometricAdvectDistribution', 'MakeGeometry', 'MarkVoidPoints', 'Oxidation', 'OxidationConstrainedAmbient', 'OxidationCouplingParameters', 'OxidationDeformation', 'OxidationDeformationParameters', 'OxidationDiffusion', 'OxidationMaskBending', 'OxidationMaskParameters', 'OxidationModel', 'OxidationParameters', 'OxidationPresets', 'Plane', 'PointCloud', 'PrepareStencilLocalLaxFriedrichs', 'Prune', 'ReactionBoundarySample', 'Reader', 'Reduce', 'RemoveStrayPoints', 'Sphere', 'SphereDistribution', 'StencilLocalLaxFriedrichsScalar', 'ToDiskMesh', 'ToHullMesh', 'ToMesh', 'ToMultiSurfaceMesh', 'ToSurfaceMesh', 'ToVoxelMesh', 'WriteVisualizationMesh', 'Writer', 'computeLOCOSOpenWindowConservation', 'hrleGrid'] class Advect: @typing.overload def __init__(self) -> None: @@ -895,6 +900,220 @@ class MarkVoidPoints: """ Set the logic by which to choose the surface which is non-void. All other connected surfaces will then be marked as void points. """ +class Oxidation: + def __init__(self, siInterface: Domain, ambientInterface: Domain, maskInterface: Domain) -> None: + ... + def apply(self, advectionTime: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + def applyCFLLimited(self, requestedTime: typing.SupportsFloat | typing.SupportsIndex, cflFactor: typing.SupportsFloat | typing.SupportsIndex) -> float: + """ + Execute one CFL-limited LOCOS step; returns the actual time advanced. + """ + def getDeformationField(self) -> OxidationDeformation: + ... + def getDiffusionField(self) -> OxidationDiffusion: + ... + def getLastMaxVelocity(self) -> float: + """ + Maximum interface velocity (Β΅m/hr) from the most recent CFL step. + """ + def getMaskBendingField(self) -> OxidationMaskBending: + ... + def getMaskCouplingIterations(self) -> int: + ... + def getMaskCouplingResidual(self) -> float: + ... + def setAmbientInterface(self, arg0: Domain) -> None: + ... + def setCouplingParameters(self, arg0: viennals._core.OxidationCouplingParameters) -> None: + ... + def setDeformationParameters(self, arg0: viennals._core.OxidationDeformationParameters) -> None: + ... + def setGpuMode(self, mode: viennals._core.GpuMode) -> None: + ... + def setGpuPreconditioner(self, preconditioner: viennals._core.GpuPreconditioner) -> None: + ... + def setMaskBendingBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> None: + ... + def setMaskCouplingIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def setMaskCouplingTolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + def setMaskInterface(self, arg0: Domain) -> None: + ... + def setMaskParameters(self, arg0: viennals._core.OxidationMaskParameters) -> None: + ... + def setOxidationParameters(self, arg0: viennals._core.OxidationParameters) -> None: + ... + def setSiInterface(self, arg0: Domain) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> None: + ... + def setSpatialScheme(self, arg0: viennals._core.SpatialSchemeEnum) -> None: + ... + def setTemporalScheme(self, arg0: viennals._core.TemporalSchemeEnum) -> None: + ... +class OxidationConstrainedAmbient(viennals._core.VelocityField): + def __init__(self, deformationField: OxidationDeformation, maskVelocityField: OxidationMaskBending, maskInterface: Domain, maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... +class OxidationDeformation(viennals._core.VelocityField): + def __init__(self, reactionInterface: Domain, ambientInterface: Domain, diffusionField: OxidationDiffusion, oxidationParameters: viennals._core.OxidationParameters, deformationParameters: viennals._core.OxidationDeformationParameters = ...) -> None: + ... + def apply(self) -> None: + ... + def avgExpansionSpeed(self) -> float: + ... + def clearMaskInterface(self) -> None: + ... + def clearMaskVelocityField(self) -> None: + ... + def clearSolveBounds(self) -> None: + ... + def getIterations(self) -> int: + ... + def getNumberOfSolutionNodes(self) -> int: + ... + def getPressure(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def getResidual(self) -> float: + ... + def getStrainRateTensor(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> typing.Annotated[list[float], "FixedSize(9)"]: + ... + def getStrainTrace(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def getStressTensor(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> typing.Annotated[list[float], "FixedSize(9)"]: + ... + def getVelocity(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> typing.Annotated[list[float], "FixedSize(3)"]: + ... + def getVonMisesStress(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def markGeometryChanged(self) -> None: + ... + def setAmbientInterface(self, arg0: Domain) -> None: + ... + def setDeformationParameters(self, arg0: viennals._core.OxidationDeformationParameters) -> None: + ... + def setDiffusionField(self, arg0: OxidationDiffusion) -> None: + ... + def setMaskInterface(self, maskInterface: Domain, maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... + def setMaskVelocityField(self, arg0: viennals._core.VelocityField) -> None: + ... + def setOxidationParameters(self, arg0: viennals._core.OxidationParameters) -> None: + ... + def setOxideSigns(self, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def setReactionInterface(self, arg0: Domain) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> None: + ... + def writeFieldsToLevelSet(self) -> None: + ... +class OxidationDiffusion(viennals._core.VelocityField): + def __init__(self, reactionInterface: Domain, ambientInterface: Domain, parameters: viennals._core.OxidationParameters = ...) -> None: + ... + def apply(self) -> None: + ... + def clearMaskInterface(self) -> None: + ... + def clearPressureField(self) -> None: + ... + def clearSolveBounds(self) -> None: + ... + def getConcentration(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def getConcentrationCache(self) -> dict[int, float]: + ... + def getEffectiveReactionRate(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def getIterations(self) -> int: + ... + def getNumberOfSolutionNodes(self) -> int: + ... + def getParameters(self) -> viennals._core.OxidationParameters: + ... + def getReactionBoundarySample(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> ReactionBoundarySample: + ... + def getResidual(self) -> float: + ... + def getScalarVelocityFromSample(self, arg0: ReactionBoundarySample) -> float: + ... + def markGeometryChanged(self) -> None: + ... + def markSolved(self) -> None: + ... + def setAmbientInterface(self, arg0: Domain) -> None: + ... + def setConcentrationCache(self, arg0: collections.abc.Mapping[typing.SupportsInt | typing.SupportsIndex, typing.SupportsFloat | typing.SupportsIndex]) -> None: + ... + def setMaskInterface(self, maskInterface: Domain, maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... + def setOxideSigns(self, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def setParameters(self, arg0: viennals._core.OxidationParameters) -> None: + ... + def setPressure(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"], arg1: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + def setReactionInterface(self, arg0: Domain) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> None: + ... + def writePersistentFields(self) -> None: + ... +class OxidationMaskBending(viennals._core.VelocityField): + @typing.overload + def __init__(self, deformationField: OxidationDeformation, maskParameters: viennals._core.OxidationMaskParameters = ...) -> None: + ... + @typing.overload + def __init__(self, deformationField: OxidationDeformation, maskInterface: Domain, maskParameters: viennals._core.OxidationMaskParameters = ..., maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... + def apply(self) -> None: + ... + def clearSolveBounds(self) -> None: + ... + def getIterations(self) -> int: + ... + def getLastApplyVelocityChange(self) -> float: + ... + def getNumberOfContactNodes(self) -> int: + ... + def getNumberOfFixedNodes(self) -> int: + ... + def getNumberOfSolutionNodes(self) -> int: + ... + def getParameters(self) -> viennals._core.OxidationMaskParameters: + ... + def getResidual(self) -> float: + ... + def setAmbientInterface(self, ambientInterface: Domain, ambientSign: typing.SupportsInt | typing.SupportsIndex = -1) -> None: + ... + def setMaskInterface(self, maskInterface: Domain, maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... + def setParameters(self, arg0: viennals._core.OxidationMaskParameters) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> None: + ... + def writeFieldsToLevelSet(self) -> None: + ... +class OxidationModel: + def __init__(self, diffusionField: OxidationDiffusion, deformationField: OxidationDeformation, couplingParameters: viennals._core.OxidationCouplingParameters = ...) -> None: + ... + def apply(self) -> None: + ... + def clearSolveBounds(self) -> None: + ... + def getIterations(self) -> int: + ... + def getResidual(self) -> float: + ... + def setDeformationField(self, arg0: OxidationDeformation) -> None: + ... + def setDiffusionField(self, arg0: OxidationDiffusion) -> None: + ... + def setParameters(self, arg0: viennals._core.OxidationCouplingParameters) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> None: + ... class Plane: def __init__(self, origin: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], normal: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None: ... @@ -918,6 +1137,40 @@ class Prune: """ Set levelset to prune. """ +class ReactionBoundarySample: + found: bool + def __init__(self) -> None: + ... + @property + def concentration(self) -> float: + ... + @concentration.setter + def concentration(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def crossingAxis(self) -> int: + ... + @crossingAxis.setter + def crossingAxis(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def crossingOffset(self) -> int: + ... + @crossingOffset.setter + def crossingOffset(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def distance(self) -> float: + ... + @distance.setter + def distance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def nodeIndex(self) -> typing.Annotated[list[int], "FixedSize(2)"]: + ... + @nodeIndex.setter + def nodeIndex(self, arg1: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(2)"]) -> None: + ... class Reader: @typing.overload def __init__(self) -> None: @@ -1286,4 +1539,6 @@ def FinalizeStencilLocalLaxFriedrichs(levelSets: collections.abc.Sequence[Domain ... def PrepareStencilLocalLaxFriedrichs(levelSets: collections.abc.Sequence[Domain], isDepo: collections.abc.Sequence[bool]) -> None: ... +def computeLOCOSOpenWindowConservation(siInitial: Domain, siAfter: Domain, ambientInitial: Domain, ambientAfter: Domain, xMin: typing.SupportsFloat | typing.SupportsIndex, xMax: typing.SupportsFloat | typing.SupportsIndex, jMin: typing.SupportsInt | typing.SupportsIndex, jMax: typing.SupportsInt | typing.SupportsIndex, expansionCoefficient: typing.SupportsFloat | typing.SupportsIndex) -> viennals._core.LOCOSConservationDiagnostics: + ... CompareArea = CompareVolume diff --git a/python/viennals/d3.pyi b/python/viennals/d3.pyi index 8ea88dfb..bcaf1c2c 100644 --- a/python/viennals/d3.pyi +++ b/python/viennals/d3.pyi @@ -5,7 +5,12 @@ from __future__ import annotations import collections.abc import typing import viennals._core -__all__: list[str] = ['Advect', 'BooleanOperation', 'Box', 'BoxDistribution', 'CalculateCurvatures', 'CalculateNormalVectors', 'CalculateVisibilities', 'Check', 'CompareChamfer', 'CompareCriticalDimensions', 'CompareNarrowBand', 'CompareSparseField', 'CompareVolume', 'ConvexHull', 'CustomSphereDistribution', 'Cylinder', 'DetectFeatures', 'Domain', 'Expand', 'FinalizeStencilLocalLaxFriedrichs', 'FromMesh', 'FromSurfaceMesh', 'FromVolumeMesh', 'GeometricAdvect', 'GeometricAdvectDistribution', 'MakeGeometry', 'MarkVoidPoints', 'Plane', 'PointCloud', 'PrepareStencilLocalLaxFriedrichs', 'Prune', 'Reader', 'Reduce', 'RemoveStrayPoints', 'Sphere', 'SphereDistribution', 'StencilLocalLaxFriedrichsScalar', 'ToDiskMesh', 'ToHullMesh', 'ToMesh', 'ToMultiSurfaceMesh', 'ToSurfaceMesh', 'ToVoxelMesh', 'WriteVisualizationMesh', 'Writer', 'hrleGrid'] +from viennals._core import OxidationCouplingParameters +from viennals._core import OxidationDeformationParameters +from viennals._core import OxidationMaskParameters +from viennals._core import OxidationParameters +from viennals._core import OxidationPresets +__all__: list[str] = ['Advect', 'BooleanOperation', 'Box', 'BoxDistribution', 'CalculateCurvatures', 'CalculateNormalVectors', 'CalculateVisibilities', 'Check', 'CompareChamfer', 'CompareCriticalDimensions', 'CompareNarrowBand', 'CompareSparseField', 'CompareVolume', 'ConvexHull', 'CustomSphereDistribution', 'Cylinder', 'DetectFeatures', 'Domain', 'Expand', 'FinalizeStencilLocalLaxFriedrichs', 'FromMesh', 'FromSurfaceMesh', 'FromVolumeMesh', 'GeometricAdvect', 'GeometricAdvectDistribution', 'MakeGeometry', 'MarkVoidPoints', 'Oxidation', 'OxidationConstrainedAmbient', 'OxidationCouplingParameters', 'OxidationDeformation', 'OxidationDeformationParameters', 'OxidationDiffusion', 'OxidationMaskBending', 'OxidationMaskParameters', 'OxidationModel', 'OxidationParameters', 'OxidationPresets', 'Plane', 'PointCloud', 'PrepareStencilLocalLaxFriedrichs', 'Prune', 'ReactionBoundarySample', 'Reader', 'Reduce', 'RemoveStrayPoints', 'Sphere', 'SphereDistribution', 'StencilLocalLaxFriedrichsScalar', 'ToDiskMesh', 'ToHullMesh', 'ToMesh', 'ToMultiSurfaceMesh', 'ToSurfaceMesh', 'ToVoxelMesh', 'WriteVisualizationMesh', 'Writer', 'hrleGrid'] class Advect: @typing.overload def __init__(self) -> None: @@ -895,6 +900,220 @@ class MarkVoidPoints: """ Set the logic by which to choose the surface which is non-void. All other connected surfaces will then be marked as void points. """ +class Oxidation: + def __init__(self, siInterface: Domain, ambientInterface: Domain, maskInterface: Domain) -> None: + ... + def apply(self, advectionTime: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + def applyCFLLimited(self, requestedTime: typing.SupportsFloat | typing.SupportsIndex, cflFactor: typing.SupportsFloat | typing.SupportsIndex) -> float: + """ + Execute one CFL-limited LOCOS step; returns the actual time advanced. + """ + def getDeformationField(self) -> OxidationDeformation: + ... + def getDiffusionField(self) -> OxidationDiffusion: + ... + def getLastMaxVelocity(self) -> float: + """ + Maximum interface velocity (Β΅m/hr) from the most recent CFL step. + """ + def getMaskBendingField(self) -> OxidationMaskBending: + ... + def getMaskCouplingIterations(self) -> int: + ... + def getMaskCouplingResidual(self) -> float: + ... + def setAmbientInterface(self, arg0: Domain) -> None: + ... + def setCouplingParameters(self, arg0: viennals._core.OxidationCouplingParameters) -> None: + ... + def setDeformationParameters(self, arg0: viennals._core.OxidationDeformationParameters) -> None: + ... + def setGpuMode(self, mode: viennals._core.GpuMode) -> None: + ... + def setGpuPreconditioner(self, preconditioner: viennals._core.GpuPreconditioner) -> None: + ... + def setMaskBendingBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"]) -> None: + ... + def setMaskCouplingIterations(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def setMaskCouplingTolerance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + def setMaskInterface(self, arg0: Domain) -> None: + ... + def setMaskParameters(self, arg0: viennals._core.OxidationMaskParameters) -> None: + ... + def setOxidationParameters(self, arg0: viennals._core.OxidationParameters) -> None: + ... + def setSiInterface(self, arg0: Domain) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"]) -> None: + ... + def setSpatialScheme(self, arg0: viennals._core.SpatialSchemeEnum) -> None: + ... + def setTemporalScheme(self, arg0: viennals._core.TemporalSchemeEnum) -> None: + ... +class OxidationConstrainedAmbient(viennals._core.VelocityField): + def __init__(self, deformationField: OxidationDeformation, maskVelocityField: OxidationMaskBending, maskInterface: Domain, maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... +class OxidationDeformation(viennals._core.VelocityField): + def __init__(self, reactionInterface: Domain, ambientInterface: Domain, diffusionField: OxidationDiffusion, oxidationParameters: viennals._core.OxidationParameters, deformationParameters: viennals._core.OxidationDeformationParameters = ...) -> None: + ... + def apply(self) -> None: + ... + def avgExpansionSpeed(self) -> float: + ... + def clearMaskInterface(self) -> None: + ... + def clearMaskVelocityField(self) -> None: + ... + def clearSolveBounds(self) -> None: + ... + def getIterations(self) -> int: + ... + def getNumberOfSolutionNodes(self) -> int: + ... + def getPressure(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def getResidual(self) -> float: + ... + def getStrainRateTensor(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> typing.Annotated[list[float], "FixedSize(9)"]: + ... + def getStrainTrace(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def getStressTensor(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> typing.Annotated[list[float], "FixedSize(9)"]: + ... + def getVelocity(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> typing.Annotated[list[float], "FixedSize(3)"]: + ... + def getVonMisesStress(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def markGeometryChanged(self) -> None: + ... + def setAmbientInterface(self, arg0: Domain) -> None: + ... + def setDeformationParameters(self, arg0: viennals._core.OxidationDeformationParameters) -> None: + ... + def setDiffusionField(self, arg0: OxidationDiffusion) -> None: + ... + def setMaskInterface(self, maskInterface: Domain, maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... + def setMaskVelocityField(self, arg0: viennals._core.VelocityField) -> None: + ... + def setOxidationParameters(self, arg0: viennals._core.OxidationParameters) -> None: + ... + def setOxideSigns(self, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def setReactionInterface(self, arg0: Domain) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"]) -> None: + ... + def writeFieldsToLevelSet(self) -> None: + ... +class OxidationDiffusion(viennals._core.VelocityField): + def __init__(self, reactionInterface: Domain, ambientInterface: Domain, parameters: viennals._core.OxidationParameters = ...) -> None: + ... + def apply(self) -> None: + ... + def clearMaskInterface(self) -> None: + ... + def clearPressureField(self) -> None: + ... + def clearSolveBounds(self) -> None: + ... + def getConcentration(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def getConcentrationCache(self) -> dict[int, float]: + ... + def getEffectiveReactionRate(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> float: + ... + def getIterations(self) -> int: + ... + def getNumberOfSolutionNodes(self) -> int: + ... + def getParameters(self) -> viennals._core.OxidationParameters: + ... + def getReactionBoundarySample(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"]) -> ReactionBoundarySample: + ... + def getResidual(self) -> float: + ... + def getScalarVelocityFromSample(self, arg0: ReactionBoundarySample) -> float: + ... + def markGeometryChanged(self) -> None: + ... + def markSolved(self) -> None: + ... + def setAmbientInterface(self, arg0: Domain) -> None: + ... + def setConcentrationCache(self, arg0: collections.abc.Mapping[typing.SupportsInt | typing.SupportsIndex, typing.SupportsFloat | typing.SupportsIndex]) -> None: + ... + def setMaskInterface(self, maskInterface: Domain, maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... + def setOxideSigns(self, arg0: typing.SupportsInt | typing.SupportsIndex, arg1: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + def setParameters(self, arg0: viennals._core.OxidationParameters) -> None: + ... + def setPressure(self, arg0: typing.Annotated[collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], "FixedSize(3)"], arg1: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + def setReactionInterface(self, arg0: Domain) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"]) -> None: + ... + def writePersistentFields(self) -> None: + ... +class OxidationMaskBending(viennals._core.VelocityField): + @typing.overload + def __init__(self, deformationField: OxidationDeformation, maskParameters: viennals._core.OxidationMaskParameters = ...) -> None: + ... + @typing.overload + def __init__(self, deformationField: OxidationDeformation, maskInterface: Domain, maskParameters: viennals._core.OxidationMaskParameters = ..., maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... + def apply(self) -> None: + ... + def clearSolveBounds(self) -> None: + ... + def getIterations(self) -> int: + ... + def getLastApplyVelocityChange(self) -> float: + ... + def getNumberOfContactNodes(self) -> int: + ... + def getNumberOfFixedNodes(self) -> int: + ... + def getNumberOfSolutionNodes(self) -> int: + ... + def getParameters(self) -> viennals._core.OxidationMaskParameters: + ... + def getResidual(self) -> float: + ... + def setAmbientInterface(self, ambientInterface: Domain, ambientSign: typing.SupportsInt | typing.SupportsIndex = -1) -> None: + ... + def setMaskInterface(self, maskInterface: Domain, maskSign: typing.SupportsInt | typing.SupportsIndex = 1) -> None: + ... + def setParameters(self, arg0: viennals._core.OxidationMaskParameters) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"]) -> None: + ... + def writeFieldsToLevelSet(self) -> None: + ... +class OxidationModel: + def __init__(self, diffusionField: OxidationDiffusion, deformationField: OxidationDeformation, couplingParameters: viennals._core.OxidationCouplingParameters = ...) -> None: + ... + def apply(self) -> None: + ... + def clearSolveBounds(self) -> None: + ... + def getIterations(self) -> int: + ... + def getResidual(self) -> float: + ... + def setDeformationField(self, arg0: OxidationDeformation) -> None: + ... + def setDiffusionField(self, arg0: OxidationDiffusion) -> None: + ... + def setParameters(self, arg0: viennals._core.OxidationCouplingParameters) -> None: + ... + def setSolveBounds(self, minIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"], maxIndex: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"]) -> None: + ... class Plane: def __init__(self, origin: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex], normal: collections.abc.Sequence[typing.SupportsFloat | typing.SupportsIndex]) -> None: ... @@ -918,6 +1137,40 @@ class Prune: """ Set levelset to prune. """ +class ReactionBoundarySample: + found: bool + def __init__(self) -> None: + ... + @property + def concentration(self) -> float: + ... + @concentration.setter + def concentration(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def crossingAxis(self) -> int: + ... + @crossingAxis.setter + def crossingAxis(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def crossingOffset(self) -> int: + ... + @crossingOffset.setter + def crossingOffset(self, arg0: typing.SupportsInt | typing.SupportsIndex) -> None: + ... + @property + def distance(self) -> float: + ... + @distance.setter + def distance(self, arg0: typing.SupportsFloat | typing.SupportsIndex) -> None: + ... + @property + def nodeIndex(self) -> typing.Annotated[list[int], "FixedSize(3)"]: + ... + @nodeIndex.setter + def nodeIndex(self, arg1: typing.Annotated[collections.abc.Sequence[typing.SupportsInt | typing.SupportsIndex], "FixedSize(3)"]) -> None: + ... class Reader: @typing.overload def __init__(self) -> None: diff --git a/tests/DefineInterior/CMakeLists.txt b/tests/DefineInterior/CMakeLists.txt new file mode 100644 index 00000000..3f3334b6 --- /dev/null +++ b/tests/DefineInterior/CMakeLists.txt @@ -0,0 +1,8 @@ +project(DefineInterior LANGUAGES CXX) + +# VTK output example (simple direct VTK writing) +add_executable(${PROJECT_NAME} "${PROJECT_NAME}.cpp") +target_link_libraries(${PROJECT_NAME} PRIVATE ViennaLS) + +add_dependencies(ViennaLS_Tests ${PROJECT_NAME}) +add_test(NAME ${PROJECT_NAME} COMMAND $) diff --git a/tests/DefineInterior/DefineInterior.cpp b/tests/DefineInterior/DefineInterior.cpp new file mode 100644 index 00000000..17896ca3 --- /dev/null +++ b/tests/DefineInterior/DefineInterior.cpp @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace viennals; + +template +void testInteriorDefinition(const std::string &suffix, size_t expectedBefore, + size_t expectedAfter) { + using NumericType = double; + + auto circle = SmartPointer>::New(1.0); + NumericType origin[D]; + for (int i = 0; i < D; ++i) + origin[i] = 0.0; + auto sphere = SmartPointer>::New(origin, 5.0); + MakeGeometry(circle, sphere).apply(); + + auto &domain = circle->getDomain(); + auto &grid = circle->getGrid(); + viennahrle::ConstSparseStarIterator< + typename Domain::DomainType, 1> + itBefore(domain, grid.getMinGridPoint()); + viennahrle::Index endIdx = grid.incrementIndices(grid.getMaxGridPoint()); + size_t interiorBefore = 0; + while (itBefore.getIndices() < endIdx) { + if (itBefore.getCenter().isDefined() && + itBefore.getCenter().getValue() < 0) { + interiorBefore++; + } + itBefore.next(); + } + + VC_TEST_ASSERT(interiorBefore == expectedBefore) + + { + auto mesh = SmartPointer>::New(); + ToMesh(circle, mesh).apply(); + VTKWriter(mesh, "interior_before_" + suffix + ".vtk").apply(); + } + + Interior filling(circle); + filling.apply(); + + LSTEST_ASSERT_VALID_LS(circle, NumericType, D) + + viennahrle::ConstSparseStarIterator< + typename Domain::DomainType, 1> + itAfter(circle->getDomain(), grid.getMinGridPoint()); + size_t interiorAfter = 0; + while (itAfter.getIndices() < endIdx) { + if (itAfter.getCenter().isDefined() && itAfter.getCenter().getValue() < 0) { + interiorAfter++; + } + itAfter.next(); + } + + { + auto mesh = SmartPointer>::New(); + ToMesh(circle, mesh).apply(); + VTKWriter(mesh, "interior_after_" + suffix + ".vtk").apply(); + } + + VC_TEST_ASSERT(interiorAfter == expectedAfter) + + std::cout << "D=" << D << ": " << interiorBefore << " β†’ " << interiorAfter + << " interior points\n"; +} + +int main() { + testInteriorDefinition<2>("2D", 24, 69); + testInteriorDefinition<3>("3D", 210, 485); + + return 0; +} diff --git a/tests/Oxidation/CMakeLists.txt b/tests/Oxidation/CMakeLists.txt new file mode 100644 index 00000000..175783c1 --- /dev/null +++ b/tests/Oxidation/CMakeLists.txt @@ -0,0 +1,7 @@ +project(Oxidation LANGUAGES CXX) + +add_executable(${PROJECT_NAME} "${PROJECT_NAME}.cpp") +target_link_libraries(${PROJECT_NAME} PRIVATE ViennaLS) + +add_dependencies(ViennaLS_Tests ${PROJECT_NAME}) +add_test(NAME ${PROJECT_NAME} COMMAND $) diff --git a/tests/Oxidation/Oxidation.cpp b/tests/Oxidation/Oxidation.cpp new file mode 100644 index 00000000..dd1f3529 --- /dev/null +++ b/tests/Oxidation/Oxidation.cpp @@ -0,0 +1,465 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace ls = viennals; + +using NumericType = double; +constexpr int D = 2; + +using LevelSet = ls::SmartPointer>; + +// --------------------------------------------------------------------------- +// Geometry helpers +// --------------------------------------------------------------------------- + +LevelSet makePlane(const double *bounds, + ls::Domain::BoundaryType *bc, + NumericType gridDelta, NumericType y) { + auto lsd = ls::Domain::New(bounds, bc, gridDelta); + ls::MakeGeometry( + lsd, + ls::Plane::New(ls::VectorType{0., y}, + ls::VectorType{0., 1.})) + .apply(); + return lsd; +} + +LevelSet makeBox(const double *bounds, + ls::Domain::BoundaryType *bc, + NumericType gridDelta, ls::VectorType minPt, + ls::VectorType maxPt) { + auto lsd = ls::Domain::New(bounds, bc, gridDelta); + ls::MakeGeometry mg( + lsd, ls::Box::New(minPt, maxPt)); + std::array ignoreBC{}; + ignoreBC[1] = true; + mg.setIgnoreBoundaryConditions(ignoreBC); + mg.apply(); + return lsd; +} + +// --------------------------------------------------------------------------- +// Standard (no-mask) oxidation: verify growth and volume conservation. +// +// Setup: flat Si at y=0, flat SiO2 surface at y=oxThick. +// Run one CFL-limited step; check: +// - actualDt > 0 +// - CFL limiting fires when requested step >> CFL step +// - Si interface receded (Si was consumed) +// - Ambient interface rose (oxide grew upward) +// - Volume is conserved: Si recession Γ— (Ξ²βˆ’1) β‰ˆ ambient lift +// - getMaskBendingField() returns nullptr (no mask) +// --------------------------------------------------------------------------- + +void testStandardOxidation() { + constexpr NumericType gridDelta = 0.1; + constexpr NumericType extent = 2.0; + constexpr NumericType oxThick = 0.5; // 5 grid cells β€” well resolved + constexpr NumericType yMin = -0.5; + constexpr NumericType yMax = oxThick + 0.5; + + double bounds[2 * D] = {-extent, extent, yMin, yMax}; + ls::Domain::BoundaryType bc[D]; + bc[0] = ls::Domain::BoundaryType::REFLECTIVE_BOUNDARY; + bc[1] = ls::Domain::BoundaryType::INFINITE_BOUNDARY; + + auto si = makePlane(bounds, bc, gridDelta, 0.); + auto ambient = makePlane(bounds, bc, gridDelta, oxThick); + + // Deep copies for before/after comparison. + auto siInitial = ls::Domain::New(si); + auto ambientInitial = ls::Domain::New(ambient); + + auto oxParams = ls::OxidationPresets::wet1000CDealGrove100(); + oxParams.velocitySign = -1.; + oxParams.tolerance = 1e-7; + + auto defParams = ls::OxidationPresets::oxideMechanics1000C(1.); + defParams.mechanicsIterations = 200; + defParams.mechanicsTolerance = 1e-2; + defParams.pressureTolerance = 1e-6; + defParams.stokesTolerance = 1e-6; + defParams.pressureIterations = 300; + defParams.stokesIterations = 80; + + ls::OxidationCouplingParameters coupling; + coupling.maxIterations = 8; + coupling.tolerance = 5e-2; + coupling.relaxation = 1.0; + + // --- Standard mode: no mask --- + auto ox = ls::Oxidation::New(si, ambient); + ox->setOxidationParameters(oxParams); + ox->setDeformationParameters(defParams); + ox->setCouplingParameters(coupling); + + // Request 0.5 hr: CFL limit is ~0.4 hr, so this should be CFL-limited. + const NumericType actualDt = ox->applyCFLLimited(0.5, 0.499); + + VC_TEST_ASSERT(actualDt > NumericType(0)) + VC_TEST_ASSERT(actualDt < NumericType(0.5)) + + // No mask β†’ getMaskBendingField() must be null. + VC_TEST_ASSERT(ox->getMaskBendingField() == nullptr) + + // Solver fields should be populated. + VC_TEST_ASSERT(ox->getDiffusionField() != nullptr) + VC_TEST_ASSERT(ox->getDeformationField() != nullptr) + VC_TEST_ASSERT(ox->getDiffusionField()->getNumberOfSolutionNodes() > 0) + + // Measure interface positions using column-scan interpolation. + const auto toIndex = [gridDelta](NumericType v) { + return static_cast(std::llround(v / gridDelta)); + }; + const viennahrle::IndexType jMin = toIndex(yMin); + const viennahrle::IndexType jMax = toIndex(yMax); + + // Volume conservation: Si recession Γ— (Ξ²βˆ’1) β‰ˆ ambient lift. + // Sample x ∈ [0, extentΓ—0.8] β€” away from the reflective boundary artefacts. + const auto diag = ls::computeLOCOSOpenWindowConservation( + siInitial, si, ambientInitial, ambient, NumericType(0), + extent * NumericType(0.8), jMin, jMax, oxParams.expansionCoefficient); + + VC_TEST_ASSERT(diag.samples > 0) + VC_TEST_ASSERT(diag.siliconRecession > NumericType(0)) // Si was consumed + VC_TEST_ASSERT(diag.ambientLift > NumericType(0)) // oxide grew + VC_TEST_ASSERT(diag.relativeError < + NumericType(0.15)) // ≀15% conservation error + + LSTEST_ASSERT_VALID_LS(si, NumericType, D) + LSTEST_ASSERT_VALID_LS(ambient, NumericType, D) +} + +// --------------------------------------------------------------------------- +// LOCOS (mask) oxidation: verify mask coupling and open-window growth. +// +// Setup: Si at y=0, pad oxide at y=padOx, Si₃Nβ‚„ mask box over x≀0. +// Run one CFL-limited step; check: +// - actualDt > 0 +// - getMaskBendingField() is populated (mask was used) +// - Mask coupling converged within iteration budget +// - Oxide grew in the open window (x>0) +// - Level sets remain valid after the step +// --------------------------------------------------------------------------- + +void testLOCOSOxidation() { + constexpr NumericType gridDelta = 0.1; + constexpr NumericType xExtent = 1.5; + constexpr NumericType yMin = -0.5; + constexpr NumericType yMax = 1.0; + constexpr NumericType padOx = 0.15; // 1.5 grid cells + constexpr NumericType maskThick = 0.2; + constexpr NumericType maskEdge = 0.; + constexpr NumericType maskContactEpsilon = 1e-6; + constexpr NumericType advectionTime = 0.2; + + double bounds[2 * D] = {-xExtent, xExtent, yMin, yMax}; + ls::Domain::BoundaryType bc[D]; + bc[0] = ls::Domain::BoundaryType::REFLECTIVE_BOUNDARY; + bc[1] = ls::Domain::BoundaryType::INFINITE_BOUNDARY; + + auto si = makePlane(bounds, bc, gridDelta, 0.); + + auto ambient = ls::Domain::New(si); + auto sphere = + ls::SmartPointer>::New( + padOx); + ls::GeometricAdvect(ambient, sphere).apply(); + + auto siInitial = ls::Domain::New(si); + auto ambientInitial = ls::Domain::New(ambient); + + auto mask = + makeBox(bounds, bc, gridDelta, {-xExtent, padOx - maskContactEpsilon}, + {maskEdge, padOx + maskThick}); + + auto oxParams = ls::OxidationPresets::wet1000CDealGrove100(); + oxParams.velocitySign = -1.; + oxParams.maskTransferCoefficient = 0.; + oxParams.maskConcentration = 0.; + oxParams.tolerance = 1e-7; + + auto defParams = ls::OxidationPresets::oxideMechanics1000C(advectionTime); + defParams.mechanicsIterations = 200; + defParams.mechanicsTolerance = 1e-2; + defParams.pressureTolerance = 1e-6; + defParams.stokesTolerance = 1e-6; + defParams.pressureIterations = 200; + defParams.stokesIterations = 60; + + ls::OxidationCouplingParameters coupling; + coupling.maxIterations = 8; + coupling.tolerance = 5e-2; + coupling.relaxation = 1.0; + + auto maskParams = ls::OxidationPresets::siliconNitrideMask1000C(); + maskParams.maxIterations = 4000; + + const auto toIndex = [gridDelta](NumericType v) { + return static_cast(std::llround(v / gridDelta)); + }; + + // --- LOCOS mode: provide mask --- + auto ox = ls::Oxidation::New(si, ambient, mask); + ox->setOxidationParameters(oxParams); + ox->setDeformationParameters(defParams); + ox->setCouplingParameters(coupling); + ox->setMaskParameters(maskParams); + viennahrle::Index diffMin{toIndex(-xExtent), toIndex(yMin)}; + viennahrle::Index diffMax{toIndex(xExtent), toIndex(yMax)}; + ox->setSolveBounds(diffMin, diffMax); + + viennahrle::Index mbMin{toIndex(-xExtent), toIndex(padOx) - 1}; + viennahrle::Index mbMax{toIndex(maskEdge), toIndex(padOx + maskThick) + 1}; + ox->setMaskBendingBounds(mbMin, mbMax); + ox->setMaskCouplingIterations(8); + ox->setMaskCouplingTolerance(5e-2); + + const NumericType actualDt = ox->applyCFLLimited(advectionTime, 0.499); + + VC_TEST_ASSERT(actualDt > NumericType(0)) + VC_TEST_ASSERT(actualDt <= advectionTime) + + // Mask bending field must be populated in LOCOS mode. + VC_TEST_ASSERT(ox->getMaskBendingField() != nullptr) + VC_TEST_ASSERT(ox->getMaskCouplingIterations() <= 8) + VC_TEST_ASSERT(ox->getMaskCouplingResidual() < NumericType(5e-2)) + + // Volume conservation in the open window (x > 0.4 Β΅m, well inside window). + const auto diag = ls::computeLOCOSOpenWindowConservation( + siInitial, si, ambientInitial, ambient, NumericType(0.4), + NumericType(1.2), toIndex(yMin), toIndex(yMax), + oxParams.expansionCoefficient); + + VC_TEST_ASSERT(diag.samples > 0) + VC_TEST_ASSERT(diag.siliconRecession > NumericType(0)) + VC_TEST_ASSERT(diag.ambientLift > NumericType(0)) + VC_TEST_ASSERT(diag.relativeError < NumericType(0.15)) + + LSTEST_ASSERT_VALID_LS(si, NumericType, D) + LSTEST_ASSERT_VALID_LS(ambient, NumericType, D) + LSTEST_ASSERT_VALID_LS(mask, NumericType, D) +} + +// --------------------------------------------------------------------------- +// LOCOS mask mechanics sensitivity. +// +// The mask contact geometry is identical for all cases: same bottom surface, +// same mask edge, same pad oxide. Only the mask top surface or viscosity +// changes. The traction-driven contact solve should therefore make thicker +// and more viscous masks less mobile than a thin/soft mask. +// --------------------------------------------------------------------------- + +struct LOCOSMaskResponse { + NumericType actualDt = 0.; + NumericType verticalMaskSpeed = 0.; + NumericType underMaskOxideSpeed = 0.; + NumericType maxMaskSpeed = 0.; + std::size_t maskNodes = 0; + std::size_t contactNodes = 0; + std::size_t fixedNodes = 0; +}; + +LOCOSMaskResponse runLOCOSMaskResponse(NumericType maskThick, + NumericType maskViscosity) { + constexpr NumericType gridDelta = 0.1; + constexpr NumericType xExtent = 1.5; + constexpr NumericType yMin = -0.5; + constexpr NumericType yMax = 1.2; + constexpr NumericType padOx = 0.15; + constexpr NumericType maskEdge = 0.; + constexpr NumericType maskContactEpsilon = 1e-6; + constexpr NumericType advectionTime = 0.12; + + double bounds[2 * D] = {-xExtent, xExtent, yMin, yMax}; + ls::Domain::BoundaryType bc[D]; + bc[0] = ls::Domain::BoundaryType::REFLECTIVE_BOUNDARY; + bc[1] = ls::Domain::BoundaryType::INFINITE_BOUNDARY; + + auto si = makePlane(bounds, bc, gridDelta, 0.); + auto ambient = ls::Domain::New(si); + auto sphere = + ls::SmartPointer>::New( + padOx); + ls::GeometricAdvect(ambient, sphere).apply(); + + auto mask = + makeBox(bounds, bc, gridDelta, {-xExtent, padOx - maskContactEpsilon}, + {maskEdge, padOx + maskThick}); + + auto oxParams = ls::OxidationPresets::wet1000CDealGrove100(); + oxParams.velocitySign = -1.; + oxParams.maskTransferCoefficient = 0.; + oxParams.maskConcentration = 0.; + oxParams.tolerance = 1e-7; + + auto defParams = ls::OxidationPresets::oxideMechanics1000C(advectionTime); + defParams.mechanicsIterations = 200; + defParams.mechanicsTolerance = 1e-2; + defParams.pressureTolerance = 1e-6; + defParams.stokesTolerance = 1e-6; + defParams.pressureIterations = 200; + defParams.stokesIterations = 60; + + ls::OxidationCouplingParameters coupling; + coupling.maxIterations = 8; + coupling.tolerance = 5e-2; + coupling.relaxation = 1.0; + + auto maskParams = ls::OxidationPresets::siliconNitrideMask1000C(); + maskParams.contactMode = 1; + maskParams.referenceViscosity = maskViscosity; + maskParams.anchorBoundaryDirection = 0; + maskParams.anchorBoundarySide = -1; + maskParams.anchorBoundaryLayers = 1; + maskParams.maxIterations = 4000; + + const auto toIndex = [gridDelta](NumericType v) { + return static_cast(std::llround(v / gridDelta)); + }; + + auto ox = ls::Oxidation::New(si, ambient, mask); + ox->setOxidationParameters(oxParams); + ox->setDeformationParameters(defParams); + ox->setCouplingParameters(coupling); + ox->setMaskParameters(maskParams); + viennahrle::Index diffMin{toIndex(-xExtent), toIndex(yMin)}; + viennahrle::Index diffMax{toIndex(xExtent), toIndex(yMax)}; + ox->setSolveBounds(diffMin, diffMax); + viennahrle::Index mbMin{toIndex(-xExtent), toIndex(padOx) - 1}; + viennahrle::Index mbMax{toIndex(maskEdge), toIndex(padOx + maskThick) + 1}; + ox->setMaskBendingBounds(mbMin, mbMax); + ox->setMaskCouplingIterations(8); + ox->setMaskCouplingTolerance(5e-2); + + LOCOSMaskResponse response; + response.actualDt = ox->applyCFLLimited(advectionTime, 0.499); + auto maskField = ox->getMaskBendingField(); + VC_TEST_ASSERT(maskField != nullptr) + + const ls::Vec3D zero{0., 0., 0.}; + response.verticalMaskSpeed = maskField->getDissipationAlpha(1, -1, zero); + response.maxMaskSpeed = std::max(maskField->getDissipationAlpha(0, -1, zero), + response.verticalMaskSpeed); + auto deformationField = ox->getDeformationField(); + VC_TEST_ASSERT(deformationField != nullptr) + const ls::Vec3D underMaskPoint{ + NumericType(-0.2), padOx - gridDelta * NumericType(0.25), 0.}; + const auto underMaskVelocity = + deformationField->getVectorVelocity(underMaskPoint, -1, zero, 0); + response.underMaskOxideSpeed = std::abs(underMaskVelocity[1]); + response.maskNodes = maskField->getNumberOfSolutionNodes(); + response.contactNodes = maskField->getNumberOfContactNodes(); + response.fixedNodes = maskField->getNumberOfFixedNodes(); + return response; +} + +void testLOCOSMaskStiffnessSensitivity() { + const auto thin = runLOCOSMaskResponse(NumericType(0.2), NumericType(5.e11)); + const auto thick = runLOCOSMaskResponse(NumericType(0.4), NumericType(5.e11)); + const auto soft = runLOCOSMaskResponse(NumericType(0.2), NumericType(2.5e11)); + + VC_TEST_ASSERT(thin.actualDt > NumericType(0)) + VC_TEST_ASSERT(thick.actualDt > NumericType(0)) + VC_TEST_ASSERT(soft.actualDt > NumericType(0)) + VC_TEST_ASSERT(thin.contactNodes > 0) + VC_TEST_ASSERT(thin.fixedNodes > 0) + VC_TEST_ASSERT(thick.maskNodes > thin.maskNodes) + VC_TEST_ASSERT(thin.verticalMaskSpeed > NumericType(0)) + VC_TEST_ASSERT(thin.underMaskOxideSpeed > NumericType(0)) + VC_TEST_ASSERT(soft.verticalMaskSpeed > thin.verticalMaskSpeed) + VC_TEST_ASSERT(thick.verticalMaskSpeed < thin.verticalMaskSpeed) + VC_TEST_ASSERT(soft.underMaskOxideSpeed > thin.underMaskOxideSpeed) + VC_TEST_ASSERT(thick.underMaskOxideSpeed < thin.underMaskOxideSpeed) +} + +// --------------------------------------------------------------------------- +// 3D smoke test: flat Si substrate, one CFL step, no mask. +// +// Exercises the 3D face-enumeration and Stokes-solve code paths that are +// entirely absent from the 2D tests. Checks only that the solver runs, +// Si was consumed, and both level sets remain valid β€” not conservation or +// rate accuracy (the coarse grid makes those unreliable at this small size). +// --------------------------------------------------------------------------- + +void testStandard3D() { + constexpr int D3 = 3; + using LS3 = ls::SmartPointer>; + + constexpr NumericType gridDelta = 0.15; + constexpr NumericType extent = 0.75; + constexpr NumericType oxThick = 0.30; + constexpr NumericType yMin = -0.30; + constexpr NumericType yMax = oxThick + 0.30; + + double bounds[2 * D3] = {-extent, extent, yMin, yMax, -extent, extent}; + ls::Domain::BoundaryType bc[D3]; + bc[0] = ls::Domain::BoundaryType::REFLECTIVE_BOUNDARY; + bc[1] = ls::Domain::BoundaryType::INFINITE_BOUNDARY; + bc[2] = ls::Domain::BoundaryType::REFLECTIVE_BOUNDARY; + + auto si = ls::Domain::New(bounds, bc, gridDelta); + ls::MakeGeometry( + si, ls::Plane::New( + ls::VectorType{0., 0., 0.}, + ls::VectorType{0., 1., 0.})) + .apply(); + + auto ambient = ls::Domain::New(si); + auto sphere = + ls::SmartPointer>::New( + oxThick); + ls::GeometricAdvect(ambient, sphere).apply(); + + auto siInitial = ls::Domain::New(si); + + auto oxParams = ls::OxidationPresets::wet1000CDealGrove100(); + oxParams.velocitySign = -1.; + oxParams.tolerance = 1e-7; + + auto defParams = ls::OxidationPresets::oxideMechanics1000C(0.1); + defParams.mechanicsIterations = 200; + defParams.mechanicsTolerance = 1e-2; + defParams.pressureTolerance = 1e-6; + defParams.stokesTolerance = 1e-6; + defParams.pressureIterations = 300; + defParams.stokesIterations = 150; + + ls::OxidationCouplingParameters coupling; + coupling.maxIterations = 8; + coupling.tolerance = 5e-2; + coupling.relaxation = 1.0; + + auto ox = ls::Oxidation::New(si, ambient); + ox->setOxidationParameters(oxParams); + ox->setDeformationParameters(defParams); + ox->setCouplingParameters(coupling); + + const NumericType actualDt = ox->applyCFLLimited(0.1, 0.099); + + VC_TEST_ASSERT(actualDt > NumericType(0)) + VC_TEST_ASSERT(ox->getDiffusionField() != nullptr) + VC_TEST_ASSERT(ox->getDiffusionField()->getNumberOfSolutionNodes() > 0) + + LSTEST_ASSERT_VALID_LS(si, NumericType, D3) + LSTEST_ASSERT_VALID_LS(ambient, NumericType, D3) +} + +// --------------------------------------------------------------------------- + +int main() { + testStandardOxidation(); + testLOCOSOxidation(); + testLOCOSMaskStiffnessSensitivity(); + testStandard3D(); + return 0; +} diff --git a/tests/OxidationModel/CMakeLists.txt b/tests/OxidationModel/CMakeLists.txt new file mode 100644 index 00000000..20d11210 --- /dev/null +++ b/tests/OxidationModel/CMakeLists.txt @@ -0,0 +1,13 @@ +project(OxidationModel LANGUAGES CXX) + +add_executable(${PROJECT_NAME} "${PROJECT_NAME}.cpp") +target_link_libraries(${PROJECT_NAME} PRIVATE ViennaLS) + +add_dependencies(ViennaLS_Tests ${PROJECT_NAME}) +add_test(NAME ${PROJECT_NAME} COMMAND $) + +add_executable(OxidationConvergence OxidationConvergence.cpp) +target_link_libraries(OxidationConvergence PRIVATE ViennaLS) + +add_dependencies(ViennaLS_Tests OxidationConvergence) +add_test(NAME OxidationConvergence COMMAND $) diff --git a/tests/OxidationModel/OxidationConvergence.cpp b/tests/OxidationModel/OxidationConvergence.cpp new file mode 100644 index 00000000..b9a4b457 --- /dev/null +++ b/tests/OxidationModel/OxidationConvergence.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ls = viennals; + +using NumericType = double; +constexpr int D = 2; +using LevelSet = ls::SmartPointer>; + +namespace { + +LevelSet makePlane(const double *bounds, + ls::Domain::BoundaryType *boundaryCons, + NumericType gridDelta, NumericType y) { + auto levelSet = + ls::Domain::New(bounds, boundaryCons, gridDelta); + const ls::VectorType origin{0., y}; + const ls::VectorType normal{0., 1.}; + ls::MakeGeometry( + levelSet, ls::Plane::New(origin, normal)) + .apply(); + return levelSet; +} + +LevelSet makeMask(const double *bounds, + ls::Domain::BoundaryType *boundaryCons, + NumericType gridDelta, NumericType xMin, NumericType xMax, + NumericType yMin, NumericType yMax) { + auto mask = ls::Domain::New(bounds, boundaryCons, gridDelta); + const ls::VectorType minCorner{xMin, yMin}; + const ls::VectorType maxCorner{xMax, yMax}; + ls::MakeGeometry makeGeometry( + mask, ls::Box::New(minCorner, maxCorner)); + makeGeometry.setIgnoreBoundaryConditions(std::array{false, true}); + makeGeometry.apply(); + return mask; +} + +void testLOCOSInterfaceConvergence() { + constexpr NumericType gridDelta = 0.1; + constexpr NumericType xExtent = 1.5; + constexpr NumericType yMin = -0.5; + constexpr NumericType yMax = 1.0; + constexpr NumericType padOxideThickness = 0.15; + constexpr NumericType maskThickness = 0.2; + constexpr NumericType maskEdge = 0.; + constexpr NumericType advectionTime = 0.2; + constexpr NumericType maskContactEpsilon = 1.e-6; + + double bounds[2 * D] = {-xExtent, xExtent, yMin, yMax}; + ls::Domain::BoundaryType boundaryCons[D]; + boundaryCons[0] = + ls::Domain::BoundaryType::REFLECTIVE_BOUNDARY; + boundaryCons[1] = ls::Domain::BoundaryType::INFINITE_BOUNDARY; + + auto siInterface = makePlane(bounds, boundaryCons, gridDelta, 0.); + auto ambientInterface = ls::Domain::New(siInterface); + auto initialOxide = + ls::SmartPointer>::New( + padOxideThickness); + ls::GeometricAdvect(ambientInterface, initialOxide).apply(); + auto siInitial = ls::Domain::New(siInterface); + auto ambientInitial = ls::Domain::New(ambientInterface); + + auto maskInterface = + makeMask(bounds, boundaryCons, gridDelta, -xExtent, maskEdge, + padOxideThickness - maskContactEpsilon, + padOxideThickness + maskThickness); + + auto oxParams = ls::OxidationPresets::wet1000CDealGrove100(); + oxParams.velocitySign = -1.; + oxParams.maskTransferCoefficient = 0.; + oxParams.maskConcentration = 0.; + oxParams.tolerance = 1.e-7; + + auto defParams = ls::OxidationPresets::oxideMechanics1000C(advectionTime); + defParams.mechanicsIterations = 200; + defParams.mechanicsTolerance = 1e-2; + defParams.pressureTolerance = 1e-6; + defParams.stokesTolerance = 1e-6; + defParams.pressureIterations = 200; + defParams.stokesIterations = 60; + + ls::OxidationCouplingParameters couplingParams; + couplingParams.maxIterations = 8; + couplingParams.tolerance = 5e-2; + couplingParams.relaxation = 1.0; + + auto maskParams = ls::OxidationPresets::siliconNitrideMask1000C(); + maskParams.maxIterations = 4000; + + const auto toIndex = [](NumericType value) { + return static_cast(std::llround(value / gridDelta)); + }; + viennahrle::Index diffMinIndex{toIndex(-xExtent), toIndex(yMin)}; + viennahrle::Index diffMaxIndex{toIndex(xExtent), toIndex(yMax)}; + viennahrle::Index maskMinIndex{toIndex(-xExtent), + toIndex(padOxideThickness) - 1}; + viennahrle::Index maskMaxIndex{ + toIndex(maskEdge), toIndex(padOxideThickness + maskThickness) + 1}; + + auto locos = ls::Oxidation::New(siInterface, ambientInterface, + maskInterface); + locos->setOxidationParameters(oxParams); + locos->setDeformationParameters(defParams); + locos->setCouplingParameters(couplingParams); + locos->setMaskParameters(maskParams); + locos->setSolveBounds(diffMinIndex, diffMaxIndex); + locos->setMaskBendingBounds(maskMinIndex, maskMaxIndex); + locos->setMaskCouplingIterations(8); + locos->setMaskCouplingTolerance(5.e-2); + locos->apply(advectionTime); + + VC_TEST_ASSERT(locos->getMaskCouplingIterations() <= 8) + VC_TEST_ASSERT(locos->getMaskCouplingResidual() < 5.e-2) + + const auto conservation = ls::computeLOCOSOpenWindowConservation( + siInitial, siInterface, ambientInitial, ambientInterface, 0.4, 1.2, + toIndex(yMin), toIndex(yMax), oxParams.expansionCoefficient); + VC_TEST_ASSERT(conservation.samples > 0) + VC_TEST_ASSERT(conservation.relativeError < 0.1) +} + +} // namespace + +int main() { + testLOCOSInterfaceConvergence(); + return 0; +} diff --git a/tests/OxidationModel/OxidationModel.cpp b/tests/OxidationModel/OxidationModel.cpp new file mode 100644 index 00000000..7bab8f84 --- /dev/null +++ b/tests/OxidationModel/OxidationModel.cpp @@ -0,0 +1,241 @@ +#include +#include +#include +#include +#include +#include + +#include + +namespace ls = viennals; + +template +ls::SmartPointer> makeOxidationSolve( + const ls::SmartPointer> &reactionInterface, + const ls::SmartPointer> &ambientInterface, + ls::OxidationParameters parameters, const viennahrle::Index &minIndex, + const viennahrle::Index &maxIndex) { + auto oxidation = ls::OxidationDiffusion::New( + reactionInterface, ambientInterface, parameters); + oxidation->setSolveBounds(minIndex, maxIndex); + oxidation->apply(); + return oxidation; +} + +int main() { + constexpr int D = 2; + constexpr double gridDelta = 1.; + constexpr double extent = 8.; + constexpr double oxideThickness = 6.; + + double bounds[2 * D] = {-extent, extent, -1., oxideThickness + 1.}; + ls::Domain::BoundaryType boundaryCons[D]; + boundaryCons[0] = ls::Domain::BoundaryType::REFLECTIVE_BOUNDARY; + boundaryCons[1] = ls::Domain::BoundaryType::INFINITE_BOUNDARY; + + auto reactionInterface = + ls::Domain::New(bounds, boundaryCons, gridDelta); + auto ambientInterface = + ls::Domain::New(bounds, boundaryCons, gridDelta); + + const ls::VectorType reactionOrigin{0., 0.}; + const ls::VectorType ambientOrigin{0., oxideThickness}; + const ls::VectorType normal{0., 1.}; + + ls::MakeGeometry(reactionInterface, + ls::Plane::New(reactionOrigin, normal)) + .apply(); + ls::MakeGeometry(ambientInterface, + ls::Plane::New(ambientOrigin, normal)) + .apply(); + + ls::OxidationParameters parameters; + parameters.diffusionCoefficient = 1.; + parameters.reactionRate = 1.; + parameters.transferCoefficient = 1.; + parameters.equilibriumConcentration = 1.; + parameters.expansionCoefficient = 2.27; + parameters.maxIterations = 20000; + parameters.tolerance = 1e-10; + parameters.maxGridPoints = 100000; + + const auto wetPreset = ls::OxidationPresets::wet1000CDealGrove100(); + VC_TEST_ASSERT(std::abs(wetPreset.diffusionCoefficient - 0.157) < 1e-12) + VC_TEST_ASSERT(std::abs(wetPreset.reactionRate - 0.74) < 1e-12) + VC_TEST_ASSERT(wetPreset.reactionActivationVolume > 0.) + const auto oxidePreset = ls::OxidationPresets::oxideMechanics1000C(0.25); + VC_TEST_ASSERT(oxidePreset.viscosity > 0.) + VC_TEST_ASSERT(std::abs(oxidePreset.stressTimeStep - 0.25) < 1e-12) + const auto maskPreset = ls::OxidationPresets::siliconNitrideMask1000C(); + VC_TEST_ASSERT(maskPreset.referenceViscosity > 0.) + + auto autoBoundedOxidation = ls::OxidationDiffusion::New( + reactionInterface, ambientInterface, parameters); + autoBoundedOxidation->apply(); + VC_TEST_ASSERT(autoBoundedOxidation->getNumberOfSolutionNodes() > 0) + VC_TEST_ASSERT(autoBoundedOxidation->getResidual() < 1e-6) + + auto oxidation = ls::OxidationDiffusion::New( + reactionInterface, ambientInterface, parameters); + viennahrle::Index minIndex{-8, -1}; + viennahrle::Index maxIndex{8, 7}; + oxidation->setSolveBounds(minIndex, maxIndex); + oxidation->apply(); + + VC_TEST_ASSERT(oxidation->getNumberOfSolutionNodes() > 0) + VC_TEST_ASSERT(oxidation->getResidual() < 1e-6) + + ls::Vec3D nearReaction{0., 1., 0.}; + ls::Vec3D nearAmbient{0., oxideThickness - 1., 0.}; + ls::Vec3D reactionBoundary{0., 0., 0.}; + + const double reactionConcentration = + oxidation->getConcentration(nearReaction); + const double ambientConcentration = oxidation->getConcentration(nearAmbient); + const double reactionBoundaryConcentration = + oxidation->getReactionBoundaryConcentration(reactionBoundary); + + VC_TEST_ASSERT(reactionConcentration > 0.) + VC_TEST_ASSERT(reactionBoundaryConcentration > 0.) + VC_TEST_ASSERT(reactionBoundaryConcentration < reactionConcentration) + VC_TEST_ASSERT(ambientConcentration > reactionConcentration) + VC_TEST_ASSERT(ambientConcentration < parameters.equilibriumConcentration) + + const double velocity = + oxidation->getScalarVelocity(nearReaction, 0, {0., 1., 0.}, 0); + VC_TEST_ASSERT(velocity > 0.) + const double boundaryVelocity = + oxidation->getScalarVelocity(reactionBoundary, 0, {0., 1., 0.}, 0); + const double expectedBoundaryVelocity = + parameters.reactionRate * reactionBoundaryConcentration / + (parameters.oxidantMoleculeDensity * parameters.expansionCoefficient); + VC_TEST_ASSERT(std::abs(boundaryVelocity - expectedBoundaryVelocity) < 1.e-10) + + ls::OxidationDeformationParameters deformationParameters; + deformationParameters.viscosity = 1e3; + deformationParameters.mechanicsIterations = 10; + deformationParameters.pressureIterations = 500; + deformationParameters.stokesIterations = 100; + deformationParameters.tolerance = 1e-8; + + auto deformation = ls::OxidationDeformation::New( + reactionInterface, ambientInterface, oxidation, parameters, + deformationParameters); + deformation->setSolveBounds(minIndex, maxIndex); + deformation->apply(); + + VC_TEST_ASSERT(deformation->getNumberOfSolutionNodes() > 0) + VC_TEST_ASSERT(std::isfinite(deformation->getResidual())) + VC_TEST_ASSERT(deformation->getResidual() < 2.) + + // The (gamma-1)/gamma expansion ratio must be checked at the same y location + // as the deformation boundary velocity: the reaction interface node at y=0. + // Querying at y=1 vs y=0 uses different concentrations and breaks the ratio. + const double siliconSpeed = std::abs( + oxidation->getScalarVelocity(reactionBoundary, 0, {0., 1., 0.}, 0)); + const double ambientSpeed = std::abs( + deformation->getVectorVelocity(reactionBoundary, 0, {0., 1., 0.}, 0)[1]); + const double oxidePressure = deformation->getPressure(nearReaction); + const auto stressTensor = deformation->getStressTensor(nearReaction); + const double vonMisesStress = deformation->getVonMisesStress(nearReaction); + VC_TEST_ASSERT(std::isfinite(oxidePressure)) + VC_TEST_ASSERT(std::abs(oxidePressure) > 0.) + VC_TEST_ASSERT(std::isfinite(stressTensor[0])) + VC_TEST_ASSERT(std::isfinite(stressTensor[4])) + VC_TEST_ASSERT(std::isfinite(vonMisesStress)) + VC_TEST_ASSERT(vonMisesStress >= 0.) + + const double ambientFraction = ambientSpeed / (ambientSpeed + siliconSpeed); + const double siliconFraction = siliconSpeed / (ambientSpeed + siliconSpeed); + const double expectedAmbientFraction = + (parameters.expansionCoefficient - 1.) / parameters.expansionCoefficient; + const double expectedSiliconFraction = 1. / parameters.expansionCoefficient; + + VC_TEST_ASSERT(std::abs(ambientFraction - expectedAmbientFraction) < 0.03) + VC_TEST_ASSERT(std::abs(siliconFraction - expectedSiliconFraction) < 0.03) + + auto maskInterface = + ls::Domain::New(bounds, boundaryCons, gridDelta); + double maskMin[D] = {2., -1.}; + double maskMax[D] = {extent, oxideThickness + 1.}; + ls::MakeGeometry(maskInterface, + ls::Box::New(maskMin, maskMax)) + .apply(); + + auto maskedOxidation = ls::OxidationDiffusion::New( + reactionInterface, ambientInterface, parameters); + maskedOxidation->setMaskInterface(maskInterface, -1); + maskedOxidation->setSolveBounds(minIndex, maxIndex); + maskedOxidation->apply(); + + VC_TEST_ASSERT(maskedOxidation->getNumberOfSolutionNodes() < + oxidation->getNumberOfSolutionNodes()) + const ls::Vec3D nearMaskedAmbient{-1., oxideThickness - 1., 0.}; + VC_TEST_ASSERT(maskedOxidation->getConcentration(nearMaskedAmbient) > 0.) + + ls::OxidationDeformationParameters maskedDeformationParameters = + deformationParameters; + auto maskedDeformation = ls::OxidationDeformation::New( + reactionInterface, ambientInterface, maskedOxidation, parameters, + maskedDeformationParameters); + maskedDeformation->setMaskInterface(maskInterface, -1); + maskedDeformation->setSolveBounds(minIndex, maxIndex); + maskedDeformation->apply(); + VC_TEST_ASSERT(maskedDeformation->getNumberOfSolutionNodes() < + deformation->getNumberOfSolutionNodes()) + VC_TEST_ASSERT(std::isfinite(maskedDeformation->getResidual())) + + ls::OxidationParameters stressParameters = parameters; + stressParameters.reactionActivationVolume = 1e-29; + auto stressOxidation = ls::OxidationDiffusion::New( + reactionInterface, ambientInterface, stressParameters); + stressOxidation->setSolveBounds(minIndex, maxIndex); + stressOxidation->apply(); + const double unstressedRate = + stressOxidation->getEffectiveReactionRate(nearReaction); + stressOxidation->setPressure(nearReaction, 1e9); + const double compressedRate = + stressOxidation->getEffectiveReactionRate(nearReaction); + VC_TEST_ASSERT(compressedRate < unstressedRate) + + auto looseParameters = parameters; + looseParameters.tolerance = 1e-7; + auto looseOxidation = makeOxidationSolve( + reactionInterface, ambientInterface, looseParameters, minIndex, maxIndex); + auto tightParameters = parameters; + tightParameters.tolerance = 1e-11; + auto tightOxidation = makeOxidationSolve( + reactionInterface, ambientInterface, tightParameters, minIndex, maxIndex); + const double looseVelocity = + looseOxidation->getScalarVelocity(nearReaction, 0, {0., 1., 0.}, 0); + const double tightVelocity = + tightOxidation->getScalarVelocity(nearReaction, 0, {0., 1., 0.}, 0); + VC_TEST_ASSERT(std::abs(looseVelocity - tightVelocity) / + std::max(std::abs(tightVelocity), 1e-12) < + 1e-3) + + auto regularizedParameters = parameters; + regularizedParameters.minBoundaryDistance = 1e-3; + auto regularizedOxidation = + makeOxidationSolve(reactionInterface, ambientInterface, + regularizedParameters, minIndex, maxIndex); + const double regularizedVelocity = + regularizedOxidation->getScalarVelocity(nearReaction, 0, {0., 1., 0.}, 0); + VC_TEST_ASSERT(std::abs(regularizedVelocity - velocity) / + std::max(std::abs(velocity), 1e-12) < + 1e-3) + + ls::OxidationCouplingParameters couplingParameters; + couplingParameters.maxIterations = 2; + couplingParameters.tolerance = 1e12; + auto coupled = ls::OxidationModel::New(oxidation, deformation, + couplingParameters); + coupled->setSolveBounds(minIndex, maxIndex); + coupled->apply(); + VC_TEST_ASSERT(coupled->getIterations() < couplingParameters.maxIterations) + + LSTEST_ASSERT_VALID_LS(reactionInterface, double, D) + LSTEST_ASSERT_VALID_LS(ambientInterface, double, D) + + return 0; +} diff --git a/tests/TimeIntegration/TimeIntegration.cpp b/tests/TimeIntegration/TimeIntegration.cpp index b7733584..ba044cbd 100644 --- a/tests/TimeIntegration/TimeIntegration.cpp +++ b/tests/TimeIntegration/TimeIntegration.cpp @@ -41,7 +41,7 @@ int main() { omp_set_num_threads(8); // Define grid and domain bounds - double gridDelta = 0.1; + double gridDelta = 0.15; double bounds[2 * D] = {-5.0, 5.0, -5.0, 5.0, -5.0, 5.0}; ls::Domain::BoundaryType boundaryCons[D]; for (int i = 0; i < D; ++i) @@ -54,11 +54,11 @@ int main() { T radius = 1.5; ls::MakeGeometry(sphere, ls::Sphere::New(origin, radius)).apply(); - // Create analytical reference for error calculation at t=2.0. - // The sphere starts at x=0 and moves with v=1 for t=2, so it ends at x=2. + // Create analytical reference for error calculation at t=0.5. + // The sphere starts at x=0 and moves with v=1 for t=0.5, so it ends at x=0.5. auto sphereRef = ls::SmartPointer>::New(bounds, boundaryCons, gridDelta); - T originRef[3] = {2.0, 0.0, 0.0}; + T originRef[3] = {0.5, 0.0, 0.0}; ls::MakeGeometry(sphereRef, ls::Sphere::New(originRef, radius)) .apply(); @@ -85,7 +85,7 @@ int main() { ls::Advect advectFE; advectFE.insertNextLevelSet(sphereFE); advectFE.setVelocityField(velocityField); - advectFE.setAdvectionTime(2.0); + advectFE.setAdvectionTime(0.5); advectFE.setSpatialScheme(ls::SpatialSchemeEnum::ENGQUIST_OSHER_1ST_ORDER); advectFE.setTemporalScheme(ls::TemporalSchemeEnum::FORWARD_EULER); @@ -93,7 +93,7 @@ int main() { ls::Advect advectRK2; advectRK2.insertNextLevelSet(sphereRK2); advectRK2.setVelocityField(velocityField); - advectRK2.setAdvectionTime(2.0); + advectRK2.setAdvectionTime(0.5); advectRK2.setSpatialScheme(ls::SpatialSchemeEnum::ENGQUIST_OSHER_1ST_ORDER); advectRK2.setTemporalScheme(ls::TemporalSchemeEnum::RUNGE_KUTTA_2ND_ORDER); @@ -101,7 +101,7 @@ int main() { ls::Advect advectRK3; advectRK3.insertNextLevelSet(sphereRK3); advectRK3.setVelocityField(velocityField); - advectRK3.setAdvectionTime(2.0); + advectRK3.setAdvectionTime(0.5); advectRK3.setSpatialScheme(ls::SpatialSchemeEnum::ENGQUIST_OSHER_1ST_ORDER); advectRK3.setTemporalScheme(ls::TemporalSchemeEnum::RUNGE_KUTTA_3RD_ORDER); @@ -109,7 +109,7 @@ int main() { ls::Advect advectWENO3_FE; advectWENO3_FE.insertNextLevelSet(sphereWENO3_FE); advectWENO3_FE.setVelocityField(velocityField); - advectWENO3_FE.setAdvectionTime(2.0); + advectWENO3_FE.setAdvectionTime(0.5); advectWENO3_FE.setSpatialScheme(ls::SpatialSchemeEnum::WENO_3RD_ORDER); advectWENO3_FE.setTemporalScheme(ls::TemporalSchemeEnum::FORWARD_EULER); @@ -117,7 +117,7 @@ int main() { ls::Advect advectWENO3_RK2; advectWENO3_RK2.insertNextLevelSet(sphereWENO3_RK2); advectWENO3_RK2.setVelocityField(velocityField); - advectWENO3_RK2.setAdvectionTime(2.0); + advectWENO3_RK2.setAdvectionTime(0.5); advectWENO3_RK2.setSpatialScheme(ls::SpatialSchemeEnum::WENO_3RD_ORDER); advectWENO3_RK2.setTemporalScheme( ls::TemporalSchemeEnum::RUNGE_KUTTA_2ND_ORDER); @@ -126,7 +126,7 @@ int main() { ls::Advect advectWENO3_RK3; advectWENO3_RK3.insertNextLevelSet(sphereWENO3_RK3); advectWENO3_RK3.setVelocityField(velocityField); - advectWENO3_RK3.setAdvectionTime(2.0); + advectWENO3_RK3.setAdvectionTime(0.5); advectWENO3_RK3.setSpatialScheme(ls::SpatialSchemeEnum::WENO_3RD_ORDER); advectWENO3_RK3.setTemporalScheme( ls::TemporalSchemeEnum::RUNGE_KUTTA_3RD_ORDER); @@ -135,7 +135,7 @@ int main() { ls::Advect advectWENO5_FE; advectWENO5_FE.insertNextLevelSet(sphereWENO5_FE); advectWENO5_FE.setVelocityField(velocityField); - advectWENO5_FE.setAdvectionTime(2.0); + advectWENO5_FE.setAdvectionTime(0.5); advectWENO5_FE.setSpatialScheme(ls::SpatialSchemeEnum::WENO_5TH_ORDER); advectWENO5_FE.setTemporalScheme(ls::TemporalSchemeEnum::FORWARD_EULER); @@ -143,7 +143,7 @@ int main() { ls::Advect advectWENO5_RK2; advectWENO5_RK2.insertNextLevelSet(sphereWENO5_RK2); advectWENO5_RK2.setVelocityField(velocityField); - advectWENO5_RK2.setAdvectionTime(2.0); + advectWENO5_RK2.setAdvectionTime(0.5); advectWENO5_RK2.setSpatialScheme(ls::SpatialSchemeEnum::WENO_5TH_ORDER); advectWENO5_RK2.setTemporalScheme( ls::TemporalSchemeEnum::RUNGE_KUTTA_2ND_ORDER); @@ -152,7 +152,7 @@ int main() { ls::Advect advectWENO5_RK3; advectWENO5_RK3.insertNextLevelSet(sphereWENO5_RK3); advectWENO5_RK3.setVelocityField(velocityField); - advectWENO5_RK3.setAdvectionTime(2.0); + advectWENO5_RK3.setAdvectionTime(0.5); advectWENO5_RK3.setSpatialScheme(ls::SpatialSchemeEnum::WENO_5TH_ORDER); advectWENO5_RK3.setTemporalScheme( ls::TemporalSchemeEnum::RUNGE_KUTTA_3RD_ORDER); @@ -174,7 +174,7 @@ int main() { chamferFE.apply(); std::cout << "Chamfer distance: " << chamferFE.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferFE.getChamferDistance() < 0.04); + VC_TEST_ASSERT(chamferFE.getChamferDistance() < 0.08); std::cout << "Running Runge-Kutta 2 Advection..." << std::endl; timer.start(); @@ -191,7 +191,7 @@ int main() { chamferRK2.apply(); std::cout << "Chamfer distance: " << chamferRK2.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferRK2.getChamferDistance() < 0.07); + VC_TEST_ASSERT(chamferRK2.getChamferDistance() < 0.14); std::cout << "Running Runge-Kutta 3 Advection..." << std::endl; timer.start(); @@ -208,7 +208,7 @@ int main() { chamferRK3.apply(); std::cout << "Chamfer distance: " << chamferRK3.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferRK3.getChamferDistance() < 0.07); + VC_TEST_ASSERT(chamferRK3.getChamferDistance() < 0.14); std::cout << "Running WENO3 Forward Euler Advection..." << std::endl; timer.start(); @@ -225,7 +225,7 @@ int main() { chamferWENO3_FE.apply(); std::cout << "Chamfer distance: " << chamferWENO3_FE.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferWENO3_FE.getChamferDistance() < 0.03); + VC_TEST_ASSERT(chamferWENO3_FE.getChamferDistance() < 0.06); std::cout << "Running WENO3 Runge-Kutta 2 Advection..." << std::endl; timer.start(); @@ -242,7 +242,7 @@ int main() { chamferWENO3_RK2.apply(); std::cout << "Chamfer distance: " << chamferWENO3_RK2.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferWENO3_RK2.getChamferDistance() < 0.008); + VC_TEST_ASSERT(chamferWENO3_RK2.getChamferDistance() < 0.016); std::cout << "Running WENO3 Runge-Kutta 3 Advection..." << std::endl; timer.start(); @@ -259,7 +259,7 @@ int main() { chamferWENO3_RK3.apply(); std::cout << "Chamfer distance: " << chamferWENO3_RK3.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferWENO3_RK3.getChamferDistance() < 0.008); + VC_TEST_ASSERT(chamferWENO3_RK3.getChamferDistance() < 0.016); std::cout << "Running WENO5 Forward Euler Advection..." << std::endl; timer.start(); @@ -276,7 +276,7 @@ int main() { chamferWENO5_FE.apply(); std::cout << "Chamfer distance: " << chamferWENO5_FE.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferWENO5_FE.getChamferDistance() < 0.02); + VC_TEST_ASSERT(chamferWENO5_FE.getChamferDistance() < 0.04); std::cout << "Running WENO5 Runge-Kutta 2 Advection..." << std::endl; timer.start(); @@ -293,7 +293,7 @@ int main() { chamferWENO5_RK2.apply(); std::cout << "Chamfer distance: " << chamferWENO5_RK2.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferWENO5_RK2.getChamferDistance() < 0.004); + VC_TEST_ASSERT(chamferWENO5_RK2.getChamferDistance() < 0.008); std::cout << "Running WENO5 Runge-Kutta 3 Advection..." << std::endl; timer.start(); @@ -310,7 +310,7 @@ int main() { chamferWENO5_RK3.apply(); std::cout << "Chamfer distance: " << chamferWENO5_RK3.getChamferDistance() << std::endl; - VC_TEST_ASSERT(chamferWENO5_RK3.getChamferDistance() < 0.004); + VC_TEST_ASSERT(chamferWENO5_RK3.getChamferDistance() < 0.008); return 0; } diff --git a/vcpkg.json b/vcpkg.json index e0408443..bd5200f7 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -4,5 +4,5 @@ "dependencies": [ "vtk" ], - "builtin-baseline": "c3867e714dd3a51c272826eea77267876517ed99" + "builtin-baseline": "72d876b20f06129a1b1e0db80c7075fe3e419d7c" } \ No newline at end of file