From 91cca831435e96ff8af9f0d3117b2a83d7a94e98 Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Tue, 16 Jun 2026 17:28:38 +0100 Subject: [PATCH 1/8] NNP: linked-cell neighbour cache and per-element workspace infrastructure Add nnp_cell_list (linked-cell neighbour finder with a Verlet skin) and nnp_neighbor_interface (species-pair routing and reusable per-element workspaces), with the supporting derived types and release routines on nnp_type. The persistent state lives on the nnp environment with no module SAVE, so committee and MIX force evaluations carry independent caches whose lifetime tracks nnp_env_release. --- src/CMakeLists.txt | 2 + src/nnp_cell_list.F | 563 +++++++++++++++++++++++++++++++++++ src/nnp_environment_types.F | 362 ++++++++++++++++++++-- src/nnp_neighbor_interface.F | 457 ++++++++++++++++++++++++++++ 4 files changed, 1365 insertions(+), 19 deletions(-) create mode 100644 src/nnp_cell_list.F create mode 100644 src/nnp_neighbor_interface.F diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 27f110f3b7..2aae42e8bf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -430,10 +430,12 @@ list( negf_subgroup_types.F negf_vectors.F nnp_acsf.F + nnp_cell_list.F nnp_environment.F nnp_environment_types.F nnp_force.F nnp_model.F + nnp_neighbor_interface.F local_gemm_api.F offload/offload_api.F optbas_fenv_manipulation.F diff --git a/src/nnp_cell_list.F b/src/nnp_cell_list.F new file mode 100644 index 0000000000..4e1dfbdfc3 --- /dev/null +++ b/src/nnp_cell_list.F @@ -0,0 +1,563 @@ +!--------------------------------------------------------------------------------------------------! +! CP2K: A general program to perform molecular dynamics simulations ! +! Copyright 2000-2026 CP2K developers group ! +! ! +! SPDX-License-Identifier: GPL-2.0-or-later ! +!--------------------------------------------------------------------------------------------------! + +! ************************************************************************************************** +!> \brief Linked-cell neighbour finder with a Verlet skin for the NNP descriptor. +!> Owns the per-nnp cell-list cache (positions, image pool, head/next +!> chain, bin geometry), rebuilds or rebins it as atoms move, and walks +!> it to fill the per-element neighbour buffers. Each &NNP force_eval +!> owns its cache, so the lifetime tracks nnp_env_release. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +!> \author Christoph Schran (christoph.schran@rub.de) +!> \date 2026-05-21 +! ************************************************************************************************** +MODULE nnp_cell_list + + USE cell_types, ONLY: cell_type,& + pbc,& + real_to_scaled + USE kinds, ONLY: dp + USE nnp_environment_types, ONLY: nnp_cell_list_cache_type,& + nnp_env_get,& + nnp_neighbor_type,& + nnp_type + USE nnp_neighbor_interface, ONLY: nnp_neigh_grp_grow +#include "./base/base_uses.f90" + + IMPLICIT NONE + + PRIVATE + + ! Verlet skin: defaults to MIN(0.5 bohr, 0.1*cutoff), or the VERLET_SKIN + ! keyword value (nnp%verlet_skin). The chain rebuilds once an atom drifts + ! more than skin/2 from its rebuild-time position. + REAL(KIND=dp), PRIVATE, PARAMETER :: default_verlet_skin = 0.5_dp + REAL(KIND=dp), PRIVATE, PARAMETER :: rebuild_eps = 1.0E-12_dp + + PUBLIC :: nnp_prepare_cell_list_cache, & + nnp_compute_neighbors_cell_list + +CONTAINS + +! ************************************************************************************************** +!> \brief Prepare the cell-list cache for the current force evaluation: wrap +!> positions into the primary cell, then either rebuild the bin geometry +!> (cell, atom-count, cutoff or large-drift change) or rebin the chain in +!> place while the Verlet skin still holds. Call once per force evaluation, +!> before any nnp_compute_neighbors_cell_list call. +!> \param nnp NNP environment whose cell_list_cache is prepared from current positions. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_prepare_cell_list_cache(nnp) + + TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp + + INTEGER :: handle, i + LOGICAL :: rebuild + REAL(KIND=dp) :: dr2_max, exact_cutoff, list_cutoff, skin + REAL(KIND=dp), DIMENSION(3) :: dr + TYPE(cell_type), POINTER :: cell + + CALL timeset('nnp_acsf_cell_list_prepare', handle) + + NULLIFY (cell) + CALL nnp_env_get(nnp_env=nnp, cell=cell) + + exact_cutoff = nnp%max_cut + IF (exact_cutoff > 0.0_dp) THEN + IF (nnp%verlet_skin >= 0.0_dp) THEN + ! VERLET_SKIN keyword value (bohr). + skin = nnp%verlet_skin + ELSE + ! Auto heuristic: cap at the default, scale down for small cutoffs. + skin = MIN(default_verlet_skin, 0.1_dp*exact_cutoff) + END IF + ELSE + skin = 0.0_dp + END IF + list_cutoff = exact_cutoff + skin + + CALL nnp_cell_list_ensure_coord_buffers(nnp%cell_list_cache, nnp%num_atoms) + + ASSOCIATE (cache => nnp%cell_list_cache) + DO i = 1, nnp%num_atoms + cache%coord_primary(:, i) = pbc(nnp%coord(:, i), cell, .TRUE.) + CALL real_to_scaled(cache%coord_scaled(:, i), & + cache%coord_primary(:, i), cell) + END DO + + rebuild = .NOT. cache%initialized + IF (.NOT. rebuild) rebuild = (cache%num_atoms /= nnp%num_atoms) + IF (.NOT. rebuild) rebuild = (ABS(cache%exact_cutoff - exact_cutoff) > rebuild_eps) + IF (.NOT. rebuild) rebuild = (ABS(cache%list_cutoff - list_cutoff) > rebuild_eps) + IF (.NOT. rebuild) rebuild = (ABS(cache%verlet_skin - skin) > rebuild_eps) + IF (.NOT. rebuild) rebuild = ANY(cache%perd /= cell%perd) + IF (.NOT. rebuild) rebuild = (cache%orthorhombic .NEQV. cell%orthorhombic) + IF (.NOT. rebuild) rebuild = ANY(ABS(cache%hmat - cell%hmat) > rebuild_eps) + IF (.NOT. rebuild) rebuild = ANY(ABS(cache%h_inv - cell%h_inv) > rebuild_eps) + + IF ((.NOT. rebuild) .AND. (skin > 0.0_dp)) THEN + dr2_max = 0.0_dp + DO i = 1, nnp%num_atoms + dr(:) = cache%coord_primary(:, i) - cache%ref_coord_primary(:, i) + dr(:) = pbc(dr, cell) + dr2_max = MAX(dr2_max, DOT_PRODUCT(dr, dr)) + END DO + IF (dr2_max > 0.25_dp*skin**2) rebuild = .TRUE. + END IF + + IF (rebuild) THEN + CALL nnp_build_cell_list_cache(cache, cell, exact_cutoff, list_cutoff, skin) + ELSE IF (skin > 0.0_dp .AND. cache%initialized) THEN + ! On skin reuse the bin geometry is frozen, but the head/next chain + ! is rebinned from current positions so the walk still finds every + ! in-cutoff pair. + CALL nnp_cell_list_rebin_chain(cache) + END IF + END ASSOCIATE + + CALL timestop(handle) + + END SUBROUTINE nnp_prepare_cell_list_cache + +! ************************************************************************************************** +!> \brief Ensure the persistent coord buffers match num_atoms. +!> Only (re-)allocates on first call or when num_atoms changes. +!> \param cache cell-list cache whose coordinate buffers will be (re-)sized +!> \param num_atoms number of primary-cell atoms the buffers must hold +! ************************************************************************************************** + SUBROUTINE nnp_cell_list_ensure_coord_buffers(cache, num_atoms) + + TYPE(nnp_cell_list_cache_type), INTENT(INOUT) :: cache + INTEGER, INTENT(IN) :: num_atoms + + IF (ALLOCATED(cache%coord_primary)) THEN + IF (SIZE(cache%coord_primary, 2) == num_atoms) RETURN + DEALLOCATE (cache%coord_primary) + END IF + IF (ALLOCATED(cache%coord_scaled)) DEALLOCATE (cache%coord_scaled) + IF (ALLOCATED(cache%ref_coord_primary)) DEALLOCATE (cache%ref_coord_primary) + + ALLOCATE (cache%coord_primary(3, num_atoms)) + ALLOCATE (cache%coord_scaled(3, num_atoms)) + ALLOCATE (cache%ref_coord_primary(3, num_atoms)) + + ! Resizing implies stale bin geometry; force the next prepare to rebuild. + cache%initialized = .FALSE. + cache%num_atoms = num_atoms + + END SUBROUTINE nnp_cell_list_ensure_coord_buffers + +! ************************************************************************************************** +!> \brief Build the Cartesian linked-cell cache from the current positions. +!> +!> Positions must already live in cache%coord_primary / coord_scaled -- +!> this routine reads them in place and resets ref_coord_primary for +!> the Verlet skin check. +!> \param cache cell-list cache to populate; coord_primary / coord_scaled must already be filled +!> \param cell simulation cell providing hmat, h_inv and per-axis periodicity +!> \param exact_cutoff largest per-pair SF cutoff used to size the exact image ring +!> \param list_cutoff cell-list bin width (exact_cutoff + Verlet skin) +!> \param skin Verlet skin allowance used by the bin-reuse displacement check +! ************************************************************************************************** + SUBROUTINE nnp_build_cell_list_cache(cache, cell, exact_cutoff, list_cutoff, skin) + + TYPE(nnp_cell_list_cache_type), INTENT(INOUT) :: cache + TYPE(cell_type), INTENT(IN), POINTER :: cell + REAL(KIND=dp), INTENT(IN) :: exact_cutoff, list_cutoff, skin + + INTEGER :: i, img, n_images, nx, ny, nz, tx, ty, tz + INTEGER, DIMENSION(3) :: bin_index, shift + REAL(KIND=dp) :: padding, safe_cutoff + REAL(KIND=dp), DIMENSION(3) :: max_ref, min_ref, range_xyz, ref_pos + + CALL nnp_cell_list_release_cell_structure(cache) + + cache%exact_cutoff = exact_cutoff + cache%list_cutoff = list_cutoff + cache%verlet_skin = skin + cache%perd = cell%perd + cache%orthorhombic = cell%orthorhombic + cache%hmat = cell%hmat + cache%h_inv = cell%h_inv + + cache%ref_coord_primary(:, :) = cache%coord_primary(:, :) + + CALL nnp_compute_pbc_copies(cache%exact_pbc_copies, cell, exact_cutoff) + CALL nnp_compute_pbc_copies(cache%list_pbc_copies, cell, list_cutoff) + ! The bin stencil clips out-of-range bins rather than wrapping, so periodic + ! neighbours come only from pre-replicated images. Force at least one ring + ! on each periodic axis (MAX, not +) so boxes with L > 2*cutoff keep their + ! wrap-around images. + cache%image_copies = MAX(cache%list_pbc_copies, cell%perd) + ! The exact image ring must fit inside the replicated pool; otherwise the + ! matcher would accept a ghost that was never created and drop a real pair. + CPASSERT(ALL(cache%exact_pbc_copies <= cache%image_copies)) + + nx = 2*cache%image_copies(1) + 1 + ny = 2*cache%image_copies(2) + 1 + nz = 2*cache%image_copies(3) + 1 + n_images = cache%num_atoms*nx*ny*nz + cache%n_images = n_images + + ALLOCATE (cache%image_atom(n_images)) + ALLOCATE (cache%image_shift(3, n_images)) + ALLOCATE (cache%image_translation(3, n_images)) + + min_ref(:) = HUGE(1.0_dp) + max_ref(:) = -HUGE(1.0_dp) + img = 0 + DO i = 1, cache%num_atoms + DO tx = -cache%image_copies(1), cache%image_copies(1) + DO ty = -cache%image_copies(2), cache%image_copies(2) + DO tz = -cache%image_copies(3), cache%image_copies(3) + img = img + 1 + shift = [tx, ty, tz] + cache%image_atom(img) = i + cache%image_shift(:, img) = shift(:) + cache%image_translation(:, img) = MATMUL(cell%hmat, REAL(shift, KIND=dp)) + ref_pos(:) = cache%coord_primary(:, i) + cache%image_translation(:, img) + min_ref(:) = MIN(min_ref(:), ref_pos(:)) + max_ref(:) = MAX(max_ref(:), ref_pos(:)) + END DO + END DO + END DO + END DO + + padding = 0.5_dp*skin + rebuild_eps + cache%lower(:) = min_ref(:) - padding + cache%upper(:) = max_ref(:) + padding + range_xyz(:) = cache%upper(:) - cache%lower(:) + + ! Coarse grid: bin width = list_cutoff (bin_span 1, 27-bin walk). A finer + ! grid changes the order in which neighbours are appended, which reorders + ! the descriptor summation, so it is not used. + safe_cutoff = MAX(list_cutoff, rebuild_eps) + DO i = 1, 3 + IF (range_xyz(i) > rebuild_eps) THEN + cache%nbin(i) = MAX(1, CEILING(range_xyz(i)/safe_cutoff)) + cache%bin_width(i) = range_xyz(i)/REAL(cache%nbin(i), KIND=dp) + cache%bin_span(i) = MIN(cache%nbin(i) - 1, & + CEILING(list_cutoff/MAX(cache%bin_width(i), rebuild_eps))) + ELSE + cache%nbin(i) = 1 + cache%bin_width(i) = 1.0_dp + cache%bin_span(i) = 0 + END IF + END DO + + cache%n_cells = PRODUCT(cache%nbin) + ALLOCATE (cache%head(cache%n_cells)) + ALLOCATE (cache%next(n_images)) + cache%head(:) = 0 + cache%next(:) = 0 + + DO img = 1, n_images + i = cache%image_atom(img) + ref_pos(:) = cache%coord_primary(:, i) + cache%image_translation(:, img) + CALL nnp_cell_list_bin_from_position(cache, ref_pos, bin_index) + cache%next(img) = cache%head(nnp_cell_list_linear_index(cache, bin_index)) + cache%head(nnp_cell_list_linear_index(cache, bin_index)) = img + END DO + + cache%initialized = .TRUE. + + END SUBROUTINE nnp_build_cell_list_cache + +! ************************************************************************************************** +!> \brief Rebin the head/next chain from current coord_primary while keeping the +!> image pool, bin geometry and ref_coord_primary frozen. Used on Verlet +!> skin reuse so the chain reflects atoms that crossed bin boundaries. +!> \param cache cell-list cache whose head/next chain is rebuilt from current coord_primary +! ************************************************************************************************** + SUBROUTINE nnp_cell_list_rebin_chain(cache) + + TYPE(nnp_cell_list_cache_type), INTENT(INOUT) :: cache + + INTEGER :: i, img, lin_idx + INTEGER, DIMENSION(3) :: bin_index + REAL(KIND=dp), DIMENSION(3) :: ref_pos + + cache%head(:) = 0 + cache%next(:) = 0 + + DO img = 1, cache%n_images + i = cache%image_atom(img) + ref_pos(:) = cache%coord_primary(:, i) + cache%image_translation(:, img) + CALL nnp_cell_list_bin_from_position(cache, ref_pos, bin_index) + lin_idx = nnp_cell_list_linear_index(cache, bin_index) + cache%next(img) = cache%head(lin_idx) + cache%head(lin_idx) = img + END DO + + END SUBROUTINE nnp_cell_list_rebin_chain + +! ************************************************************************************************** +!> \brief Free only the cell-structure pieces of the cache (bins and image +!> metadata). The coordinate buffers are persistent and kept, so a +!> rebuild can still read the current positions. +!> \param cache cell-list cache whose bin geometry and image metadata are freed (coord buffers kept) +! ************************************************************************************************** + SUBROUTINE nnp_cell_list_release_cell_structure(cache) + + TYPE(nnp_cell_list_cache_type), INTENT(INOUT) :: cache + + IF (ALLOCATED(cache%image_atom)) DEALLOCATE (cache%image_atom) + IF (ALLOCATED(cache%head)) DEALLOCATE (cache%head) + IF (ALLOCATED(cache%next)) DEALLOCATE (cache%next) + IF (ALLOCATED(cache%image_shift)) DEALLOCATE (cache%image_shift) + IF (ALLOCATED(cache%image_translation)) DEALLOCATE (cache%image_translation) + + cache%n_images = 0 + cache%n_cells = 1 + cache%exact_pbc_copies = 0 + cache%list_pbc_copies = 0 + cache%image_copies = 0 + cache%nbin = 1 + cache%bin_span = 0 + cache%lower = 0.0_dp + cache%upper = 0.0_dp + cache%bin_width = 1.0_dp + + END SUBROUTINE nnp_cell_list_release_cell_structure + +! ************************************************************************************************** +!> \brief Fill the ACSF neighbour buffers for one central atom by walking the +!> linked-cell stencil around its bin, pushing (j, dr, r) into the +!> per-species-pair slabs for entries inside each pair cutoff. The own-bin +!> self image (i == j, zero shift) is skipped. +!> \param nnp NNP environment with cell_list_cache and neighbor_interface_state ready +!> \param neighbor per-atom neighbour view filled in place (counters zeroed before this call) +!> \param i central-atom index in the global ordering +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +!> \author Christoph Schran (christoph.schran@rub.de) +! ************************************************************************************************** + SUBROUTINE nnp_compute_neighbors_cell_list(nnp, neighbor, i) + + TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp + TYPE(nnp_neighbor_type), INTENT(INOUT) :: neighbor + INTEGER, INTENT(IN) :: i + + INTEGER :: bx, by, bz, img, ind, j, neighbor_ind, & + pair_slot, s + INTEGER, DIMENSION(3) :: bin_index, current_shift + REAL(KIND=dp) :: norm + REAL(KIND=dp), DIMENSION(3) :: center, dr, image_pos + + ASSOCIATE (cache => nnp%cell_list_cache, & + state => nnp%neighbor_interface_state) + + ind = nnp%ele_ind(i) + center(:) = cache%coord_primary(:, i) + CALL nnp_cell_list_bin_from_position(cache, center, bin_index) + + DO bx = MAX(1, bin_index(1) - cache%bin_span(1)), & + MIN(cache%nbin(1), bin_index(1) + cache%bin_span(1)) + DO by = MAX(1, bin_index(2) - cache%bin_span(2)), & + MIN(cache%nbin(2), bin_index(2) + cache%bin_span(2)) + DO bz = MAX(1, bin_index(3) - cache%bin_span(3)), & + MIN(cache%nbin(3), bin_index(3) + cache%bin_span(3)) + img = cache%head(nnp_cell_list_linear_index(cache, [bx, by, bz])) + DO WHILE (img > 0) + j = cache%image_atom(img) + current_shift(:) = cache%image_shift(:, img) + IF (j == i .AND. ALL(current_shift == 0)) THEN + img = cache%next(img) + CYCLE + END IF + IF (.NOT. nnp_cell_list_exact_image_match(cache, i, j, current_shift)) THEN + img = cache%next(img) + CYCLE + END IF + + image_pos(:) = cache%coord_primary(:, j) + cache%image_translation(:, img) + dr(:) = center(:) - image_pos(:) + norm = NORM2(dr(:)) + neighbor_ind = nnp%ele_ind(j) + + IF (norm < state%pair_map(ind, neighbor_ind)%max_relevant_cutoff) THEN + DO pair_slot = 1, state%pair_map(ind, neighbor_ind)%n_rad + s = state%pair_map(ind, neighbor_ind)%rad_groups(pair_slot) + IF (norm < nnp%rad(ind)%symfgrp(s)%cutoff) THEN + neighbor%n_rad(s) = neighbor%n_rad(s) + 1 + IF (neighbor%n_rad(s) > neighbor%rad(s)%cap) & + CALL nnp_neigh_grp_grow(neighbor%rad(s), neighbor%n_rad(s)) + neighbor%rad(s)%ind(neighbor%n_rad(s)) = j + neighbor%rad(s)%dist(1, neighbor%n_rad(s)) = dr(1) + neighbor%rad(s)%dist(2, neighbor%n_rad(s)) = dr(2) + neighbor%rad(s)%dist(3, neighbor%n_rad(s)) = dr(3) + neighbor%rad(s)%dist(4, neighbor%n_rad(s)) = norm + END IF + END DO + + DO pair_slot = 1, state%pair_map(ind, neighbor_ind)%n_ang1 + s = state%pair_map(ind, neighbor_ind)%ang1_groups(pair_slot) + IF (norm < nnp%ang(ind)%symfgrp(s)%cutoff) THEN + neighbor%n_ang1(s) = neighbor%n_ang1(s) + 1 + IF (neighbor%n_ang1(s) > neighbor%ang1(s)%cap) & + CALL nnp_neigh_grp_grow(neighbor%ang1(s), neighbor%n_ang1(s)) + neighbor%ang1(s)%ind(neighbor%n_ang1(s)) = j + neighbor%ang1(s)%dist(1, neighbor%n_ang1(s)) = dr(1) + neighbor%ang1(s)%dist(2, neighbor%n_ang1(s)) = dr(2) + neighbor%ang1(s)%dist(3, neighbor%n_ang1(s)) = dr(3) + neighbor%ang1(s)%dist(4, neighbor%n_ang1(s)) = norm + END IF + END DO + + DO pair_slot = 1, state%pair_map(ind, neighbor_ind)%n_ang2 + s = state%pair_map(ind, neighbor_ind)%ang2_groups(pair_slot) + IF (norm < nnp%ang(ind)%symfgrp(s)%cutoff) THEN + neighbor%n_ang2(s) = neighbor%n_ang2(s) + 1 + IF (neighbor%n_ang2(s) > neighbor%ang2(s)%cap) & + CALL nnp_neigh_grp_grow(neighbor%ang2(s), neighbor%n_ang2(s)) + neighbor%ang2(s)%ind(neighbor%n_ang2(s)) = j + neighbor%ang2(s)%dist(1, neighbor%n_ang2(s)) = dr(1) + neighbor%ang2(s)%dist(2, neighbor%n_ang2(s)) = dr(2) + neighbor%ang2(s)%dist(3, neighbor%n_ang2(s)) = dr(3) + neighbor%ang2(s)%dist(4, neighbor%n_ang2(s)) = norm + END IF + END DO + END IF + + img = cache%next(img) + END DO + END DO + END DO + END DO + + END ASSOCIATE + + END SUBROUTINE nnp_compute_neighbors_cell_list + +! ************************************************************************************************** +!> \brief Check whether a ghost image corresponds to one of the exact pbc copies. +!> \param cache cell-list cache providing per-axis periodicity and scaled coordinates +!> \param i central-atom index in the primary cell +!> \param j candidate neighbour atom index in the primary cell +!> \param image_shift 3-vector of integer cell translations identifying the ghost image +!> \return .TRUE. if the (i, j, image_shift) image falls inside the exact_pbc_copies ring +! ************************************************************************************************** + LOGICAL FUNCTION nnp_cell_list_exact_image_match(cache, i, j, image_shift) + + TYPE(nnp_cell_list_cache_type), INTENT(IN) :: cache + INTEGER, INTENT(IN) :: i, j + INTEGER, DIMENSION(3), INTENT(IN) :: image_shift + + INTEGER :: d, minimal_shift + + nnp_cell_list_exact_image_match = .TRUE. + DO d = 1, 3 + IF (cache%perd(d) == 1) THEN + minimal_shift = NINT(cache%coord_scaled(d, i) - cache%coord_scaled(d, j)) + IF (ABS(minimal_shift - image_shift(d)) > cache%exact_pbc_copies(d)) THEN + nnp_cell_list_exact_image_match = .FALSE. + RETURN + END IF + ELSE IF (image_shift(d) /= 0) THEN + nnp_cell_list_exact_image_match = .FALSE. + RETURN + END IF + END DO + + END FUNCTION nnp_cell_list_exact_image_match + +! ************************************************************************************************** +!> \brief Convert Cartesian position to linked-cell bin coordinates. +!> \param cache cell-list cache providing the bin origin and bin widths +!> \param pos Cartesian position (Bohr) to bin +!> \param bin_index (out) 1-based bin indices clipped to [1, nbin] on each axis +! ************************************************************************************************** + SUBROUTINE nnp_cell_list_bin_from_position(cache, pos, bin_index) + + TYPE(nnp_cell_list_cache_type), INTENT(IN) :: cache + REAL(KIND=dp), DIMENSION(3), INTENT(IN) :: pos + INTEGER, DIMENSION(3), INTENT(OUT) :: bin_index + + INTEGER :: d + REAL(KIND=dp) :: scaled + + DO d = 1, 3 + scaled = (pos(d) - cache%lower(d))/MAX(cache%bin_width(d), rebuild_eps) + bin_index(d) = INT(scaled) + 1 + bin_index(d) = MAX(1, MIN(cache%nbin(d), bin_index(d))) + END DO + + END SUBROUTINE nnp_cell_list_bin_from_position + +! ************************************************************************************************** +!> \brief Flatten 3D bin indices into the head/next storage index. +!> \param cache cell-list cache providing the per-axis bin counts +!> \param bin_index 1-based 3D bin indices to flatten +!> \return linear index into cache%head +! ************************************************************************************************** + INTEGER FUNCTION nnp_cell_list_linear_index(cache, bin_index) + + TYPE(nnp_cell_list_cache_type), INTENT(IN) :: cache + INTEGER, DIMENSION(3), INTENT(IN) :: bin_index + + nnp_cell_list_linear_index = bin_index(1) + cache%nbin(1)* & + ((bin_index(2) - 1) + cache%nbin(2)*(bin_index(3) - 1)) + + END FUNCTION nnp_cell_list_linear_index + +! ************************************************************************************************** +!> \brief Number of PBC image rings along each lattice axis needed to cover a +!> sphere of radius `cutoff` from any primary-cell atom. Uses the +!> cell-vector projections onto the plane normals (correct for triclinic +!> cells) and multiplies by cell%perd so non-periodic axes return 0. +!> \param pbc_copies output: number of image rings on each of the 3 lattice axes +!> \param cell cell whose hmat/h_inv/perd/deth are read +!> \param cutoff sphere radius (Bohr) to cover +!> \author Christoph Schran (christoph.schran@rub.de) +! ************************************************************************************************** + SUBROUTINE nnp_compute_pbc_copies(pbc_copies, cell, cutoff) + + INTEGER, DIMENSION(3), INTENT(OUT) :: pbc_copies + TYPE(cell_type), INTENT(IN), POINTER :: cell + REAL(KIND=dp), INTENT(IN) :: cutoff + + REAL(KIND=dp) :: proja, projb, projc + REAL(KIND=dp), DIMENSION(3) :: axb, axc, bxc + + ! A degenerate cell (deth <= 0) makes the NORM2 normalisations below + ! divide by zero and the DO WHILE projection loops never terminate. + CPASSERT(cell%deth > 0.0_dp) + + axb(1) = cell%hmat(2, 1)*cell%hmat(3, 2) - cell%hmat(3, 1)*cell%hmat(2, 2) + axb(2) = cell%hmat(3, 1)*cell%hmat(1, 2) - cell%hmat(1, 1)*cell%hmat(3, 2) + axb(3) = cell%hmat(1, 1)*cell%hmat(2, 2) - cell%hmat(2, 1)*cell%hmat(1, 2) + axb(:) = axb(:)/NORM2(axb(:)) + + axc(1) = cell%hmat(2, 1)*cell%hmat(3, 3) - cell%hmat(3, 1)*cell%hmat(2, 3) + axc(2) = cell%hmat(3, 1)*cell%hmat(1, 3) - cell%hmat(1, 1)*cell%hmat(3, 3) + axc(3) = cell%hmat(1, 1)*cell%hmat(2, 3) - cell%hmat(2, 1)*cell%hmat(1, 3) + axc(:) = axc(:)/NORM2(axc(:)) + + bxc(1) = cell%hmat(2, 2)*cell%hmat(3, 3) - cell%hmat(3, 2)*cell%hmat(2, 3) + bxc(2) = cell%hmat(3, 2)*cell%hmat(1, 3) - cell%hmat(1, 2)*cell%hmat(3, 3) + bxc(3) = cell%hmat(1, 2)*cell%hmat(2, 3) - cell%hmat(2, 2)*cell%hmat(1, 3) + bxc(:) = bxc(:)/NORM2(bxc(:)) + + proja = ABS(SUM(cell%hmat(:, 1)*bxc(:)))*0.5_dp + projb = ABS(SUM(cell%hmat(:, 2)*axc(:)))*0.5_dp + projc = ABS(SUM(cell%hmat(:, 3)*axb(:)))*0.5_dp + + pbc_copies(:) = 0 + DO WHILE ((pbc_copies(1) + 1)*proja <= cutoff) + pbc_copies(1) = pbc_copies(1) + 1 + END DO + DO WHILE ((pbc_copies(2) + 1)*projb <= cutoff) + pbc_copies(2) = pbc_copies(2) + 1 + END DO + DO WHILE ((pbc_copies(3) + 1)*projc <= cutoff) + pbc_copies(3) = pbc_copies(3) + 1 + END DO + pbc_copies(:) = pbc_copies(:)*cell%perd(:) + + END SUBROUTINE nnp_compute_pbc_copies + +END MODULE nnp_cell_list diff --git a/src/nnp_environment_types.F b/src/nnp_environment_types.F index 8baf3ad9f2..77ee32732b 100644 --- a/src/nnp_environment_types.F +++ b/src/nnp_environment_types.F @@ -8,6 +8,7 @@ ! ************************************************************************************************** !> \brief Data types for neural network potentials !> \author Christoph Schran (christoph.schran@rub.de) +!> \author Dhruv Sharma (ds2173@cam.ac.uk) !> \date 2020-10-10 ! ************************************************************************************************** MODULE nnp_environment_types @@ -45,20 +46,29 @@ MODULE nnp_environment_types PRIVATE - LOGICAL, PRIVATE, PARAMETER :: debug_this_module = .TRUE. + LOGICAL, PRIVATE, PARAMETER :: debug_this_module = .FALSE. CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'nnp_environment_types' !> derived data types PUBLIC :: nnp_type PUBLIC :: nnp_arc_type PUBLIC :: nnp_neighbor_type + PUBLIC :: nnp_neigh_grp_type + PUBLIC :: nnp_symfgrp_type PUBLIC :: nnp_acsf_rad_type PUBLIC :: nnp_acsf_ang_type + PUBLIC :: nnp_cell_list_cache_type + PUBLIC :: nnp_neighbor_pair_map_type + PUBLIC :: nnp_dGdr_grp_type + PUBLIC :: nnp_neighbor_workspace_type + PUBLIC :: nnp_neighbor_interface_state_type ! Public subroutines *** PUBLIC :: nnp_env_release, & nnp_env_set, & - nnp_env_get + nnp_env_get, & + nnp_cell_list_cache_release, & + nnp_neighbor_interface_state_release INTEGER, PARAMETER, PUBLIC :: & nnp_cut_cos = 1, & @@ -105,9 +115,11 @@ MODULE nnp_environment_types INTEGER :: n_committee = -1 INTEGER :: n_hlayer = -1 INTEGER :: n_layer = -1 + INTEGER :: rad_spline_n = 8192 ! knots/radial group (RAD_SPLINE_N) + REAL(KIND=dp) :: verlet_skin = -1.0_dp ! cell-list skin, bohr (VERLET_SKIN; <0 = auto) INTEGER, ALLOCATABLE, DIMENSION(:) :: n_hnodes INTEGER, ALLOCATABLE, DIMENSION(:) :: actfnct - INTEGER :: expol = -1 ! extrapolation coutner + INTEGER :: expol = -1 ! extrapolation counter LOGICAL :: output_expol = .FALSE. ! output extrapolation ! structures for calculation INTEGER :: num_atoms = -1 @@ -136,6 +148,12 @@ MODULE nnp_environment_types REAL(KIND=dp) :: bias_sigma = -1.0_dp REAL(KIND=dp), DIMENSION(:, :), ALLOCATABLE :: bias_forces REAL(KIND=dp), DIMENSION(:), ALLOCATABLE :: bias_e_avrg + ! Per-nnp persistent neighbour-finder state, owned by nnp_type so committee + ! and MIX force_evals carry independent caches whose lifetime tracks + ! nnp_env_release. Allocated by nnp_prepare_neighbor_cache, freed in + ! nnp_env_release; operated on by nnp_cell_list and nnp_neighbor_interface. + TYPE(nnp_cell_list_cache_type), ALLOCATABLE :: cell_list_cache + TYPE(nnp_neighbor_interface_state_type), ALLOCATABLE :: neighbor_interface_state END TYPE nnp_type ! ************************************************************************************************** @@ -153,6 +171,26 @@ MODULE nnp_environment_types INTEGER, DIMENSION(:), ALLOCATABLE :: ele_ind CHARACTER(LEN=2), DIMENSION(:), ALLOCATABLE :: ele REAL(KIND=dp) :: cutoff = -1.0_dp + ! Packed per-member parameters: contiguous arrays sized n_symf so the inner + ! SF loop streams memory instead of indexing ang(ind)%{eta,zeta,lam,prefzeta} + ! through symf(sf). pack_izeta/pack_use_int_zeta skip NINT in the inner loop. + REAL(KIND=dp), DIMENSION(:), ALLOCATABLE :: pack_eta + REAL(KIND=dp), DIMENSION(:), ALLOCATABLE :: pack_zeta + REAL(KIND=dp), DIMENSION(:), ALLOCATABLE :: pack_lam + REAL(KIND=dp), DIMENSION(:), ALLOCATABLE :: pack_prefzeta + INTEGER, DIMENSION(:), ALLOCATABLE :: pack_izeta + LOGICAL, DIMENSION(:), ALLOCATABLE :: pack_use_int_zeta + ! Group-shared Hermite cubic spline tables (radial groups only). All SFs in + ! a group share grp%cutoff, hence one uniform grid (spline_n nodes, spacing + ! spline_dx). y(r) and y'(r) are packed sf-first so nnp_calc_rad streams them + ! after computing the interpolation parameters once per call. + LOGICAL :: spline_built = .FALSE. + INTEGER :: spline_n = 0 + REAL(KIND=dp) :: spline_dx = 1.0_dp + REAL(KIND=dp) :: spline_dx_inv = 1.0_dp + REAL(KIND=dp) :: spline_x_max = 0.0_dp + REAL(KIND=dp), DIMENSION(:, :), ALLOCATABLE :: spline_y ! (n_symf, n_grid) + REAL(KIND=dp), DIMENSION(:, :), ALLOCATABLE :: spline_dy ! (n_symf, n_grid) END TYPE nnp_symfgrp_type ! ************************************************************************************************** @@ -221,25 +259,154 @@ MODULE nnp_environment_types END TYPE nnp_acsf_ang_type ! ************************************************************************************************** -!> \brief Contains neighbors list of an atom -!> \param dist - distance vectors + norm DIM(4,nat) -!> \param n - number of neighbors +!> \brief Per-SF-group dense neighbour container. cap is the allocated slab size; +!> the live count is nnp_neighbor_type%n_rad/n_ang1/n_ang2 for the matching +!> group. ind(j) is the j-th neighbour's atom index; dist(1:3, j) is its +!> displacement and dist(4, j) its norm. Sized lazily by nnp_neigh_grp_grow. +! ************************************************************************************************** + TYPE nnp_neigh_grp_type + INTEGER :: cap = 0 + INTEGER, ALLOCATABLE, DIMENSION(:) :: ind ! (cap) + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: dist ! (4, cap) + END TYPE nnp_neigh_grp_type + +! ************************************************************************************************** +!> \brief Contains neighbors list of an atom (per-group dense layout). +!> \param n_rad/n_ang1/n_ang2 - live neighbor counts per symfgrp +!> \param rad/ang1/ang2 - per-group dense (ind, dist) containers !> \author Christoph Schran (christoph.schran@rub.de) !> \date 2020-10-10 ! ************************************************************************************************** TYPE nnp_neighbor_type - INTEGER, DIMENSION(3) :: pbc_copies = -1 - INTEGER, DIMENSION(:), ALLOCATABLE :: n_rad - INTEGER, DIMENSION(:), ALLOCATABLE :: n_ang1 - INTEGER, DIMENSION(:), ALLOCATABLE :: n_ang2 - INTEGER, DIMENSION(:, :), ALLOCATABLE :: ind_rad - INTEGER, DIMENSION(:, :), ALLOCATABLE :: ind_ang1 - INTEGER, DIMENSION(:, :), ALLOCATABLE :: ind_ang2 - REAL(KIND=dp), DIMENSION(:, :, :), ALLOCATABLE :: dist_rad - REAL(KIND=dp), DIMENSION(:, :, :), ALLOCATABLE :: dist_ang1 - REAL(KIND=dp), DIMENSION(:, :, :), ALLOCATABLE :: dist_ang2 + INTEGER, DIMENSION(3) :: pbc_copies = -1 + INTEGER, DIMENSION(:), ALLOCATABLE :: n_rad + INTEGER, DIMENSION(:), ALLOCATABLE :: n_ang1 + INTEGER, DIMENSION(:), ALLOCATABLE :: n_ang2 + TYPE(nnp_neigh_grp_type), ALLOCATABLE, DIMENSION(:) :: rad + TYPE(nnp_neigh_grp_type), ALLOCATABLE, DIMENSION(:) :: ang1 + TYPE(nnp_neigh_grp_type), ALLOCATABLE, DIMENSION(:) :: ang2 END TYPE nnp_neighbor_type +! ************************************************************************************************** +!> \brief Linked-cell + Verlet-skin cache for the NNP descriptor neighbour walk. +!> Persisted across MD steps so the image pool, bin geometry, and +!> reference positions survive between force evaluations and the +!> head/next chain can be reused as long as no atom has drifted more +!> than skin/2. Operated on by nnp_cell_list.F. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + TYPE nnp_cell_list_cache_type + LOGICAL :: initialized = .FALSE. + INTEGER :: num_atoms = -1 + INTEGER :: n_images = 0 + INTEGER :: n_cells = 1 + INTEGER, DIMENSION(3) :: exact_pbc_copies = 0 + INTEGER, DIMENSION(3) :: list_pbc_copies = 0 + INTEGER, DIMENSION(3) :: image_copies = 0 + INTEGER, DIMENSION(3) :: nbin = 1 + INTEGER, DIMENSION(3) :: bin_span = 0 + INTEGER, DIMENSION(3) :: perd = 0 + LOGICAL :: orthorhombic = .FALSE. + REAL(KIND=dp) :: exact_cutoff = -1.0_dp + REAL(KIND=dp) :: list_cutoff = -1.0_dp + REAL(KIND=dp) :: verlet_skin = 0.0_dp + REAL(KIND=dp), DIMENSION(3) :: lower = 0.0_dp + REAL(KIND=dp), DIMENSION(3) :: upper = 0.0_dp + REAL(KIND=dp), DIMENSION(3) :: bin_width = 1.0_dp + REAL(KIND=dp), DIMENSION(3, 3) :: hmat = 0.0_dp + REAL(KIND=dp), DIMENSION(3, 3) :: h_inv = 0.0_dp + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: coord_primary + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: coord_scaled + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: ref_coord_primary + INTEGER, ALLOCATABLE, DIMENSION(:) :: image_atom + INTEGER, ALLOCATABLE, DIMENSION(:) :: head + INTEGER, ALLOCATABLE, DIMENSION(:) :: next + INTEGER, ALLOCATABLE, DIMENSION(:, :) :: image_shift + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: image_translation + END TYPE nnp_cell_list_cache_type + +! ************************************************************************************************** +!> \brief Species-pair routing table. For one (central element, neighbour element) +!> pair, lists which radial / angular symmetry-function groups need that +!> neighbour and the worst-case relevant cutoff. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + TYPE nnp_neighbor_pair_map_type + INTEGER :: n_rad = 0 + INTEGER :: n_ang1 = 0 + INTEGER :: n_ang2 = 0 + REAL(KIND=dp) :: max_relevant_cutoff = 0.0_dp + INTEGER, ALLOCATABLE, DIMENSION(:) :: rad_groups + INTEGER, ALLOCATABLE, DIMENSION(:) :: ang1_groups + INTEGER, ALLOCATABLE, DIMENSION(:) :: ang2_groups + END TYPE nnp_neighbor_pair_map_type + +! ************************************************************************************************** +!> \brief Per-(element, SF-group) dG_k/dr buffer, sized to the group's n_symf (no +!> max_*_symf padding) and the observed peak neighbour count for the group. +!> Grown lazily (1.5x) so it settles at the true peak. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + TYPE nnp_dGdr_grp_type + INTEGER :: cap = 0 + INTEGER :: n_symf = 0 + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :) :: data ! (3, n_symf, cap) + END TYPE nnp_dGdr_grp_type + +! ************************************************************************************************** +!> \brief Reusable per-element scratch / persistent caches for the ACSF +!> descriptor and force assembly. See nnp_neighbor_interface for the +!> routines that populate and grow these buffers. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + TYPE nnp_neighbor_workspace_type + INTEGER :: max_rad_symf = 0 + INTEGER :: max_ang_symf = 0 + INTEGER :: n_input_nodes = 0 + ! High-water mark of the per-element angular-cache slab, tracked so the 1D + ! cutoff caches settle at the true per-element peak. + INTEGER :: cache_cap = 0 + TYPE(nnp_neighbor_type) :: neighbor + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: radial_sym + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: radial_force + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: angular_sym + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :) :: angular_force + ! Per-element persistent cutoff caches reused across this element's central + ! atoms. Sized lazily by nnp_workspace_grow_caches to MAX(n_ang1)/MAX(n_ang2). + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: fc_cache1 + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: dfc_cache1 + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: fc_cache2 + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: dfc_cache2 + ! Per-neighbour dG_k/dr storage, replacing the global + ! dsymdxyz(3, n_input_nodes, num_atoms) slab. The per-group buffers are + ! dense in (3, n_symf_for_group, n_neighbors_in_group), which shrinks the + ! working set and keeps each group walk a contiguous stride. + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: self_dGdr + TYPE(nnp_dGdr_grp_type), ALLOCATABLE, DIMENSION(:) :: dGdr_rad + TYPE(nnp_dGdr_grp_type), ALLOCATABLE, DIMENSION(:) :: dGdr_ang_jj + TYPE(nnp_dGdr_grp_type), ALLOCATABLE, DIMENSION(:) :: dGdr_ang_kk + END TYPE nnp_neighbor_workspace_type + +! ************************************************************************************************** +!> \brief Persistent neighbour-interface state. Owned by nnp_type so that the +!> per-nnp pair-routing tables and per-element workspaces survive +!> across MD steps without leaking between independent &NNP +!> force_evals. Operated on by nnp_neighbor_interface.F. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + TYPE nnp_neighbor_interface_state_type + LOGICAL :: initialized = .FALSE. + INTEGER :: n_ele = 0 + INTEGER, ALLOCATABLE, DIMENSION(:) :: n_rad + INTEGER, ALLOCATABLE, DIMENSION(:) :: n_ang + INTEGER, ALLOCATABLE, DIMENSION(:) :: n_radgrp + INTEGER, ALLOCATABLE, DIMENSION(:) :: n_anggrp + TYPE(nnp_neighbor_pair_map_type), ALLOCATABLE, & + DIMENSION(:, :) :: pair_map + TYPE(nnp_neighbor_workspace_type), ALLOCATABLE, & + DIMENSION(:) :: workspace + END TYPE nnp_neighbor_interface_state_type + ! ************************************************************************************************** !> \brief Data type for artificial neural networks !> \author Christoph Schran (christoph.schran@rub.de) @@ -283,6 +450,13 @@ CONTAINS DEALLOCATE (nnp_env%rad(i)%symfgrp(j)%symf, & nnp_env%rad(i)%symfgrp(j)%ele, & nnp_env%rad(i)%symfgrp(j)%ele_ind) + IF (ALLOCATED(nnp_env%rad(i)%symfgrp(j)%pack_eta)) & + DEALLOCATE (nnp_env%rad(i)%symfgrp(j)%pack_eta) + IF (ALLOCATED(nnp_env%rad(i)%symfgrp(j)%spline_y)) & + DEALLOCATE (nnp_env%rad(i)%symfgrp(j)%spline_y) + IF (ALLOCATED(nnp_env%rad(i)%symfgrp(j)%spline_dy)) & + DEALLOCATE (nnp_env%rad(i)%symfgrp(j)%spline_dy) + nnp_env%rad(i)%symfgrp(j)%spline_built = .FALSE. END DO DEALLOCATE (nnp_env%rad(i)%y, & nnp_env%rad(i)%funccut, & @@ -305,6 +479,18 @@ CONTAINS DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%symf, & nnp_env%ang(i)%symfgrp(j)%ele, & nnp_env%ang(i)%symfgrp(j)%ele_ind) + IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%pack_eta)) & + DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%pack_eta) + IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%pack_zeta)) & + DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%pack_zeta) + IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%pack_lam)) & + DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%pack_lam) + IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%pack_prefzeta)) & + DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%pack_prefzeta) + IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%pack_izeta)) & + DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%pack_izeta) + IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%pack_use_int_zeta)) & + DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%pack_use_int_zeta) END DO DEALLOCATE (nnp_env%ang(i)%y, & nnp_env%ang(i)%funccut, & @@ -368,11 +554,16 @@ CONTAINS IF (ALLOCATED(nnp_env%committee_forces)) DEALLOCATE (nnp_env%committee_forces) IF (ALLOCATED(nnp_env%committee_stress)) DEALLOCATE (nnp_env%committee_stress) IF (ALLOCATED(nnp_env%atoms)) DEALLOCATE (nnp_env%atoms) - IF (ALLOCATED(nnp_env%nnp_forces)) DEALLOCATE (nnp_env%nnp_forces) - IF (ASSOCIATED(nnp_env%subsys)) THEN - CALL cp_subsys_release(nnp_env%subsys) + IF (ALLOCATED(nnp_env%cell_list_cache)) THEN + CALL nnp_cell_list_cache_release(nnp_env%cell_list_cache) + DEALLOCATE (nnp_env%cell_list_cache) END IF + IF (ALLOCATED(nnp_env%neighbor_interface_state)) THEN + CALL nnp_neighbor_interface_state_release(nnp_env%neighbor_interface_state) + DEALLOCATE (nnp_env%neighbor_interface_state) + END IF + IF (ASSOCIATED(nnp_env%subsys)) THEN CALL cp_subsys_release(nnp_env%subsys) END IF @@ -385,6 +576,139 @@ CONTAINS END SUBROUTINE nnp_env_release +! ************************************************************************************************** +!> \brief Free the allocatable parts of a cell-list cache. Co-located with the +!> type definition to avoid creating a USE-cycle through nnp_cell_list. +!> \param cache cell-list cache whose allocatable image-pool and bin arrays are freed +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_cell_list_cache_release(cache) + TYPE(nnp_cell_list_cache_type), INTENT(INOUT) :: cache + + IF (ALLOCATED(cache%coord_primary)) DEALLOCATE (cache%coord_primary) + IF (ALLOCATED(cache%coord_scaled)) DEALLOCATE (cache%coord_scaled) + IF (ALLOCATED(cache%ref_coord_primary)) DEALLOCATE (cache%ref_coord_primary) + IF (ALLOCATED(cache%image_atom)) DEALLOCATE (cache%image_atom) + IF (ALLOCATED(cache%head)) DEALLOCATE (cache%head) + IF (ALLOCATED(cache%next)) DEALLOCATE (cache%next) + IF (ALLOCATED(cache%image_shift)) DEALLOCATE (cache%image_shift) + IF (ALLOCATED(cache%image_translation)) DEALLOCATE (cache%image_translation) + cache%initialized = .FALSE. + cache%num_atoms = -1 + cache%n_images = 0 + END SUBROUTINE nnp_cell_list_cache_release + +! ************************************************************************************************** +!> \brief Free the allocatable parts of a neighbour-interface state. Co-located +!> with the type definition to avoid creating a USE-cycle through +!> nnp_neighbor_interface. +!> \param state neighbour-interface state whose pair maps and per-element workspaces are freed +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_neighbor_interface_state_release(state) + TYPE(nnp_neighbor_interface_state_type), & + INTENT(INOUT) :: state + + INTEGER :: i, j + + IF (ALLOCATED(state%pair_map)) THEN + DO i = 1, SIZE(state%pair_map, 1) + DO j = 1, SIZE(state%pair_map, 2) + IF (ALLOCATED(state%pair_map(i, j)%rad_groups)) DEALLOCATE (state%pair_map(i, j)%rad_groups) + IF (ALLOCATED(state%pair_map(i, j)%ang1_groups)) DEALLOCATE (state%pair_map(i, j)%ang1_groups) + IF (ALLOCATED(state%pair_map(i, j)%ang2_groups)) DEALLOCATE (state%pair_map(i, j)%ang2_groups) + END DO + END DO + DEALLOCATE (state%pair_map) + END IF + + IF (ALLOCATED(state%workspace)) THEN + DO i = 1, SIZE(state%workspace) + CALL nnp_workspace_release(state%workspace(i)) + END DO + DEALLOCATE (state%workspace) + END IF + + IF (ALLOCATED(state%n_rad)) DEALLOCATE (state%n_rad) + IF (ALLOCATED(state%n_ang)) DEALLOCATE (state%n_ang) + IF (ALLOCATED(state%n_radgrp)) DEALLOCATE (state%n_radgrp) + IF (ALLOCATED(state%n_anggrp)) DEALLOCATE (state%n_anggrp) + + state%initialized = .FALSE. + state%n_ele = 0 + END SUBROUTINE nnp_neighbor_interface_state_release + +! ************************************************************************************************** +!> \brief Free the allocatable parts of one per-element workspace. +!> \param workspace per-element workspace whose scratch, cutoff caches and dGdr buffers are freed +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_workspace_release(workspace) + TYPE(nnp_neighbor_workspace_type), INTENT(INOUT) :: workspace + + INTEGER :: s + + IF (ALLOCATED(workspace%neighbor%rad)) THEN + DO s = 1, SIZE(workspace%neighbor%rad) + IF (ALLOCATED(workspace%neighbor%rad(s)%ind)) DEALLOCATE (workspace%neighbor%rad(s)%ind) + IF (ALLOCATED(workspace%neighbor%rad(s)%dist)) DEALLOCATE (workspace%neighbor%rad(s)%dist) + END DO + DEALLOCATE (workspace%neighbor%rad) + END IF + IF (ALLOCATED(workspace%neighbor%ang1)) THEN + DO s = 1, SIZE(workspace%neighbor%ang1) + IF (ALLOCATED(workspace%neighbor%ang1(s)%ind)) DEALLOCATE (workspace%neighbor%ang1(s)%ind) + IF (ALLOCATED(workspace%neighbor%ang1(s)%dist)) DEALLOCATE (workspace%neighbor%ang1(s)%dist) + END DO + DEALLOCATE (workspace%neighbor%ang1) + END IF + IF (ALLOCATED(workspace%neighbor%ang2)) THEN + DO s = 1, SIZE(workspace%neighbor%ang2) + IF (ALLOCATED(workspace%neighbor%ang2(s)%ind)) DEALLOCATE (workspace%neighbor%ang2(s)%ind) + IF (ALLOCATED(workspace%neighbor%ang2(s)%dist)) DEALLOCATE (workspace%neighbor%ang2(s)%dist) + END DO + DEALLOCATE (workspace%neighbor%ang2) + END IF + IF (ALLOCATED(workspace%neighbor%n_rad)) DEALLOCATE (workspace%neighbor%n_rad) + IF (ALLOCATED(workspace%neighbor%n_ang1)) DEALLOCATE (workspace%neighbor%n_ang1) + IF (ALLOCATED(workspace%neighbor%n_ang2)) DEALLOCATE (workspace%neighbor%n_ang2) + workspace%neighbor%pbc_copies = -1 + + IF (ALLOCATED(workspace%radial_sym)) DEALLOCATE (workspace%radial_sym) + IF (ALLOCATED(workspace%radial_force)) DEALLOCATE (workspace%radial_force) + IF (ALLOCATED(workspace%angular_sym)) DEALLOCATE (workspace%angular_sym) + IF (ALLOCATED(workspace%angular_force)) DEALLOCATE (workspace%angular_force) + IF (ALLOCATED(workspace%fc_cache1)) DEALLOCATE (workspace%fc_cache1) + IF (ALLOCATED(workspace%dfc_cache1)) DEALLOCATE (workspace%dfc_cache1) + IF (ALLOCATED(workspace%fc_cache2)) DEALLOCATE (workspace%fc_cache2) + IF (ALLOCATED(workspace%dfc_cache2)) DEALLOCATE (workspace%dfc_cache2) + IF (ALLOCATED(workspace%self_dGdr)) DEALLOCATE (workspace%self_dGdr) + + IF (ALLOCATED(workspace%dGdr_rad)) THEN + DO s = 1, SIZE(workspace%dGdr_rad) + IF (ALLOCATED(workspace%dGdr_rad(s)%data)) DEALLOCATE (workspace%dGdr_rad(s)%data) + END DO + DEALLOCATE (workspace%dGdr_rad) + END IF + IF (ALLOCATED(workspace%dGdr_ang_jj)) THEN + DO s = 1, SIZE(workspace%dGdr_ang_jj) + IF (ALLOCATED(workspace%dGdr_ang_jj(s)%data)) DEALLOCATE (workspace%dGdr_ang_jj(s)%data) + END DO + DEALLOCATE (workspace%dGdr_ang_jj) + END IF + IF (ALLOCATED(workspace%dGdr_ang_kk)) THEN + DO s = 1, SIZE(workspace%dGdr_ang_kk) + IF (ALLOCATED(workspace%dGdr_ang_kk(s)%data)) DEALLOCATE (workspace%dGdr_ang_kk(s)%data) + END DO + DEALLOCATE (workspace%dGdr_ang_kk) + END IF + + workspace%max_rad_symf = 0 + workspace%max_ang_symf = 0 + workspace%n_input_nodes = 0 + workspace%cache_cap = 0 + END SUBROUTINE nnp_workspace_release + ! ************************************************************************************************** !> \brief Returns various attributes of the nnp environment !> \param nnp_env ... diff --git a/src/nnp_neighbor_interface.F b/src/nnp_neighbor_interface.F new file mode 100644 index 0000000000..fecbc6b757 --- /dev/null +++ b/src/nnp_neighbor_interface.F @@ -0,0 +1,457 @@ +!--------------------------------------------------------------------------------------------------! +! CP2K: A general program to perform molecular dynamics simulations ! +! Copyright 2000-2026 CP2K developers group ! +! ! +! SPDX-License-Identifier: GPL-2.0-or-later ! +!--------------------------------------------------------------------------------------------------! + +! ************************************************************************************************** +!> \brief Per-nnp persistent neighbour-interface state for the NNP hot path. +!> Separates neighbour bookkeeping from the ACSF and network loops: +!> species-pair routing is precomputed once and per-element work +!> buffers are reused. State lives on the parent nnp_type, so each +!> &NNP force_eval keeps its own copy and the lifetime tracks +!> nnp_env_release. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +!> \author Christoph Schran (christoph.schran@rub.de) +!> \date 2026-05-21 +! ************************************************************************************************** +MODULE nnp_neighbor_interface + + USE kinds, ONLY: dp + USE nnp_environment_types, ONLY: nnp_dGdr_grp_type,& + nnp_neigh_grp_type,& + nnp_neighbor_interface_state_release,& + nnp_neighbor_workspace_type,& + nnp_type +#include "./base/base_uses.f90" + + IMPLICIT NONE + + PRIVATE + + PUBLIC :: nnp_neighbor_interface_prepare, & + nnp_neighbor_interface_reset_neighbor, & + nnp_grp_grow_dGdr, & + nnp_neigh_grp_grow, & + nnp_workspace_grow_caches + +CONTAINS + +! ************************************************************************************************** +!> \brief Ensure pair-routing metadata and reusable workspaces are ready for the current NNP model. +!> \param nnp NNP environment whose neighbor_interface_state will be (re)built if needed. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_neighbor_interface_prepare(nnp) + + TYPE(nnp_type), INTENT(INOUT) :: nnp + + LOGICAL :: rebuild + + rebuild = .NOT. nnp%neighbor_interface_state%initialized + IF (.NOT. rebuild) CALL nnp_neighbor_interface_needs_rebuild(nnp, rebuild) + + IF (rebuild) THEN + CALL nnp_neighbor_interface_state_release(nnp%neighbor_interface_state) + CALL nnp_neighbor_interface_build_pair_maps(nnp) + END IF + + END SUBROUTINE nnp_neighbor_interface_prepare + +! ************************************************************************************************** +!> \brief Reset per-group neighbour counters for one central element before refilling +!> the reusable buffers. Zeroes n_rad/n_ang1/n_ang2; the (ind, dist) slabs +!> are kept allocated and overwritten in place by the next push. +!> \param nnp NNP environment whose neighbor_interface_state will have its counters cleared. +!> \param ind central-element index (1..nnp%n_ele) whose workspace is reset. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_neighbor_interface_reset_neighbor(nnp, ind) + + TYPE(nnp_type), INTENT(INOUT) :: nnp + INTEGER, INTENT(IN) :: ind + + nnp%neighbor_interface_state%workspace(ind)%neighbor%n_rad(:) = 0 + nnp%neighbor_interface_state%workspace(ind)%neighbor%n_ang1(:) = 0 + nnp%neighbor_interface_state%workspace(ind)%neighbor%n_ang2(:) = 0 + + END SUBROUTINE nnp_neighbor_interface_reset_neighbor + +! ************************************************************************************************** +!> \brief Check whether the persistent state matches the current NNP model. Assumes +!> the per-group routing (ele_ind, cutoff) is fixed after nnp_init_acsf_groups, +!> so only element and per-element group counts are compared. +!> \param nnp NNP environment whose persistent neighbour-interface state will be inspected +!> \param rebuild (out) .TRUE. if element count or per-element group sizes have changed +! ************************************************************************************************** + SUBROUTINE nnp_neighbor_interface_needs_rebuild(nnp, rebuild) + + TYPE(nnp_type), INTENT(IN) :: nnp + LOGICAL, INTENT(OUT) :: rebuild + + INTEGER :: i + + rebuild = .FALSE. + IF (nnp%neighbor_interface_state%n_ele /= nnp%n_ele) THEN + rebuild = .TRUE. + RETURN + END IF + IF (.NOT. ALLOCATED(nnp%neighbor_interface_state%n_rad)) THEN + rebuild = .TRUE. + RETURN + END IF + + DO i = 1, nnp%n_ele + IF (nnp%neighbor_interface_state%n_rad(i) /= nnp%n_rad(i) .OR. & + nnp%neighbor_interface_state%n_ang(i) /= nnp%n_ang(i) .OR. & + nnp%neighbor_interface_state%n_radgrp(i) /= nnp%rad(i)%n_symfgrp .OR. & + nnp%neighbor_interface_state%n_anggrp(i) /= nnp%ang(i)%n_symfgrp) THEN + rebuild = .TRUE. + RETURN + END IF + END DO + + END SUBROUTINE nnp_neighbor_interface_needs_rebuild + +! ************************************************************************************************** +!> \brief Build species-pair routing tables and initialize reusable workspaces. +!> \param nnp NNP environment; pair_map and workspace arrays are (re-)allocated on its neighbor_interface_state +! ************************************************************************************************** + SUBROUTINE nnp_neighbor_interface_build_pair_maps(nnp) + + TYPE(nnp_type), INTENT(INOUT) :: nnp + + INTEGER :: i + + ASSOCIATE (state => nnp%neighbor_interface_state) + state%n_ele = nnp%n_ele + ALLOCATE (state%n_rad(nnp%n_ele)) + ALLOCATE (state%n_ang(nnp%n_ele)) + ALLOCATE (state%n_radgrp(nnp%n_ele)) + ALLOCATE (state%n_anggrp(nnp%n_ele)) + ALLOCATE (state%pair_map(nnp%n_ele, nnp%n_ele)) + ALLOCATE (state%workspace(nnp%n_ele)) + + DO i = 1, nnp%n_ele + state%n_rad(i) = nnp%n_rad(i) + state%n_ang(i) = nnp%n_ang(i) + state%n_radgrp(i) = nnp%rad(i)%n_symfgrp + state%n_anggrp(i) = nnp%ang(i)%n_symfgrp + END DO + + DO i = 1, nnp%n_ele + CALL nnp_neighbor_interface_build_pair_map_for_element(nnp, i) + CALL nnp_neighbor_interface_init_workspace_metadata(nnp, i) + END DO + + state%initialized = .TRUE. + END ASSOCIATE + + END SUBROUTINE nnp_neighbor_interface_build_pair_maps + +! ************************************************************************************************** +!> \brief Build all species-pair routes for one central element. +!> \param nnp NNP environment providing the radial/angular SF groups +!> \param ind central-element index whose pair-map row is built +! ************************************************************************************************** + SUBROUTINE nnp_neighbor_interface_build_pair_map_for_element(nnp, ind) + + TYPE(nnp_type), INTENT(INOUT) :: nnp + INTEGER, INTENT(IN) :: ind + + INTEGER :: idx, neighbor_ind, s + + DO neighbor_ind = 1, nnp%n_ele + ASSOCIATE (pair_map => nnp%neighbor_interface_state%pair_map(ind, neighbor_ind)) + pair_map%n_rad = 0 + pair_map%n_ang1 = 0 + pair_map%n_ang2 = 0 + pair_map%max_relevant_cutoff = 0.0_dp + + DO s = 1, nnp%rad(ind)%n_symfgrp + IF (nnp%rad(ind)%symfgrp(s)%ele_ind(1) == neighbor_ind) pair_map%n_rad = pair_map%n_rad + 1 + END DO + DO s = 1, nnp%ang(ind)%n_symfgrp + IF (nnp%ang(ind)%symfgrp(s)%ele_ind(1) == neighbor_ind) pair_map%n_ang1 = pair_map%n_ang1 + 1 + IF (nnp%ang(ind)%symfgrp(s)%ele_ind(2) == neighbor_ind) pair_map%n_ang2 = pair_map%n_ang2 + 1 + END DO + + ALLOCATE (pair_map%rad_groups(MAX(1, pair_map%n_rad))) + ALLOCATE (pair_map%ang1_groups(MAX(1, pair_map%n_ang1))) + ALLOCATE (pair_map%ang2_groups(MAX(1, pair_map%n_ang2))) + + idx = 0 + DO s = 1, nnp%rad(ind)%n_symfgrp + IF (nnp%rad(ind)%symfgrp(s)%ele_ind(1) == neighbor_ind) THEN + idx = idx + 1 + pair_map%rad_groups(idx) = s + pair_map%max_relevant_cutoff = MAX(pair_map%max_relevant_cutoff, nnp%rad(ind)%symfgrp(s)%cutoff) + END IF + END DO + + idx = 0 + DO s = 1, nnp%ang(ind)%n_symfgrp + IF (nnp%ang(ind)%symfgrp(s)%ele_ind(1) == neighbor_ind) THEN + idx = idx + 1 + pair_map%ang1_groups(idx) = s + pair_map%max_relevant_cutoff = MAX(pair_map%max_relevant_cutoff, nnp%ang(ind)%symfgrp(s)%cutoff) + END IF + END DO + + idx = 0 + DO s = 1, nnp%ang(ind)%n_symfgrp + IF (nnp%ang(ind)%symfgrp(s)%ele_ind(2) == neighbor_ind) THEN + idx = idx + 1 + pair_map%ang2_groups(idx) = s + pair_map%max_relevant_cutoff = MAX(pair_map%max_relevant_cutoff, nnp%ang(ind)%symfgrp(s)%cutoff) + END IF + END DO + END ASSOCIATE + END DO + + END SUBROUTINE nnp_neighbor_interface_build_pair_map_for_element + +! ************************************************************************************************** +!> \brief Cache max scratch sizes for one central element. +!> \param nnp NNP environment providing per-element SF group definitions +!> \param ind central-element index whose scratch workspace metadata is cached +! ************************************************************************************************** + SUBROUTINE nnp_neighbor_interface_init_workspace_metadata(nnp, ind) + + TYPE(nnp_type), INTENT(INOUT) :: nnp + INTEGER, INTENT(IN) :: ind + + INTEGER :: s + + ASSOCIATE (workspace => nnp%neighbor_interface_state%workspace(ind)) + workspace%max_rad_symf = 0 + workspace%max_ang_symf = 0 + workspace%n_input_nodes = nnp%n_rad(ind) + nnp%n_ang(ind) + + DO s = 1, nnp%rad(ind)%n_symfgrp + workspace%max_rad_symf = MAX(workspace%max_rad_symf, nnp%rad(ind)%symfgrp(s)%n_symf) + END DO + DO s = 1, nnp%ang(ind)%n_symfgrp + workspace%max_ang_symf = MAX(workspace%max_ang_symf, nnp%ang(ind)%symfgrp(s)%n_symf) + END DO + + ! Per-SF scratch reused inside nnp_calc_rad / nnp_calc_ang, sized by max_*_symf. + IF (ALLOCATED(workspace%radial_sym)) DEALLOCATE (workspace%radial_sym) + IF (ALLOCATED(workspace%radial_force)) DEALLOCATE (workspace%radial_force) + IF (ALLOCATED(workspace%angular_sym)) DEALLOCATE (workspace%angular_sym) + IF (ALLOCATED(workspace%angular_force)) DEALLOCATE (workspace%angular_force) + ALLOCATE (workspace%radial_sym(MAX(1, workspace%max_rad_symf))) + ALLOCATE (workspace%radial_force(3, MAX(1, workspace%max_rad_symf))) + ALLOCATE (workspace%angular_sym(MAX(1, workspace%max_ang_symf))) + ALLOCATE (workspace%angular_force(3, 3, MAX(1, workspace%max_ang_symf))) + + ! 1D angular cutoff caches; start small and grow lazily to the peak + ! per-element angular neighbour count (nnp_workspace_grow_caches). + IF (ALLOCATED(workspace%fc_cache1)) DEALLOCATE (workspace%fc_cache1) + IF (ALLOCATED(workspace%dfc_cache1)) DEALLOCATE (workspace%dfc_cache1) + IF (ALLOCATED(workspace%fc_cache2)) DEALLOCATE (workspace%fc_cache2) + IF (ALLOCATED(workspace%dfc_cache2)) DEALLOCATE (workspace%dfc_cache2) + workspace%cache_cap = 8 + ALLOCATE (workspace%fc_cache1(workspace%cache_cap)) + ALLOCATE (workspace%dfc_cache1(workspace%cache_cap)) + ALLOCATE (workspace%fc_cache2(workspace%cache_cap)) + ALLOCATE (workspace%dfc_cache2(workspace%cache_cap)) + + ! self_dGdr sized once here; independent of the candidate pool. + IF (ALLOCATED(workspace%self_dGdr)) DEALLOCATE (workspace%self_dGdr) + ALLOCATE (workspace%self_dGdr(3, MAX(1, workspace%n_input_nodes))) + + ! Per-group neighbour counters and dense (ind, dist) containers, sized to + ! n_symfgrp here; the per-group slabs grow lazily via nnp_neigh_grp_grow. + CALL nnp_release_neighbor_local(workspace) + ALLOCATE (workspace%neighbor%n_rad(MAX(1, nnp%rad(ind)%n_symfgrp))) + ALLOCATE (workspace%neighbor%n_ang1(MAX(1, nnp%ang(ind)%n_symfgrp))) + ALLOCATE (workspace%neighbor%n_ang2(MAX(1, nnp%ang(ind)%n_symfgrp))) + ALLOCATE (workspace%neighbor%rad(MAX(1, nnp%rad(ind)%n_symfgrp))) + ALLOCATE (workspace%neighbor%ang1(MAX(1, nnp%ang(ind)%n_symfgrp))) + ALLOCATE (workspace%neighbor%ang2(MAX(1, nnp%ang(ind)%n_symfgrp))) + workspace%neighbor%n_rad(:) = 0 + workspace%neighbor%n_ang1(:) = 0 + workspace%neighbor%n_ang2(:) = 0 + workspace%neighbor%pbc_copies = 0 + + ! Per-group dG/dr buffers: container arrays sized to n_symfgrp and each + ! entry's n_symf recorded. The %data slab stays unallocated until + ! nnp_grp_grow_dGdr sees a real neighbour count. + CALL nnp_release_dGdr_grp_array(workspace%dGdr_rad) + CALL nnp_release_dGdr_grp_array(workspace%dGdr_ang_jj) + CALL nnp_release_dGdr_grp_array(workspace%dGdr_ang_kk) + ALLOCATE (workspace%dGdr_rad(MAX(1, nnp%rad(ind)%n_symfgrp))) + ALLOCATE (workspace%dGdr_ang_jj(MAX(1, nnp%ang(ind)%n_symfgrp))) + ALLOCATE (workspace%dGdr_ang_kk(MAX(1, nnp%ang(ind)%n_symfgrp))) + DO s = 1, nnp%rad(ind)%n_symfgrp + workspace%dGdr_rad(s)%n_symf = nnp%rad(ind)%symfgrp(s)%n_symf + workspace%dGdr_rad(s)%cap = 0 + END DO + DO s = 1, nnp%ang(ind)%n_symfgrp + workspace%dGdr_ang_jj(s)%n_symf = nnp%ang(ind)%symfgrp(s)%n_symf + workspace%dGdr_ang_jj(s)%cap = 0 + workspace%dGdr_ang_kk(s)%n_symf = nnp%ang(ind)%symfgrp(s)%n_symf + workspace%dGdr_ang_kk(s)%cap = 0 + END DO + END ASSOCIATE + + END SUBROUTINE nnp_neighbor_interface_init_workspace_metadata + +! ************************************************************************************************** +!> \brief Release the workspace's neighbor%(rad,ang1,ang2) per-group slabs before +!> re-allocation, keeping the persistent caches that survive a rebuild. +!> \param workspace per-element scratch workspace whose neighbor%(rad,ang1,ang2) slabs will be released +! ************************************************************************************************** + SUBROUTINE nnp_release_neighbor_local(workspace) + + TYPE(nnp_neighbor_workspace_type), INTENT(INOUT) :: workspace + + INTEGER :: s + + IF (ALLOCATED(workspace%neighbor%rad)) THEN + DO s = 1, SIZE(workspace%neighbor%rad) + IF (ALLOCATED(workspace%neighbor%rad(s)%ind)) DEALLOCATE (workspace%neighbor%rad(s)%ind) + IF (ALLOCATED(workspace%neighbor%rad(s)%dist)) DEALLOCATE (workspace%neighbor%rad(s)%dist) + END DO + DEALLOCATE (workspace%neighbor%rad) + END IF + IF (ALLOCATED(workspace%neighbor%ang1)) THEN + DO s = 1, SIZE(workspace%neighbor%ang1) + IF (ALLOCATED(workspace%neighbor%ang1(s)%ind)) DEALLOCATE (workspace%neighbor%ang1(s)%ind) + IF (ALLOCATED(workspace%neighbor%ang1(s)%dist)) DEALLOCATE (workspace%neighbor%ang1(s)%dist) + END DO + DEALLOCATE (workspace%neighbor%ang1) + END IF + IF (ALLOCATED(workspace%neighbor%ang2)) THEN + DO s = 1, SIZE(workspace%neighbor%ang2) + IF (ALLOCATED(workspace%neighbor%ang2(s)%ind)) DEALLOCATE (workspace%neighbor%ang2(s)%ind) + IF (ALLOCATED(workspace%neighbor%ang2(s)%dist)) DEALLOCATE (workspace%neighbor%ang2(s)%dist) + END DO + DEALLOCATE (workspace%neighbor%ang2) + END IF + IF (ALLOCATED(workspace%neighbor%n_rad)) DEALLOCATE (workspace%neighbor%n_rad) + IF (ALLOCATED(workspace%neighbor%n_ang1)) DEALLOCATE (workspace%neighbor%n_ang1) + IF (ALLOCATED(workspace%neighbor%n_ang2)) DEALLOCATE (workspace%neighbor%n_ang2) + workspace%neighbor%pbc_copies = -1 + + END SUBROUTINE nnp_release_neighbor_local + +! ************************************************************************************************** +!> \brief Release every per-group dG/dr buffer in a container array, then deallocate the container. +!> \param grps per-group dG/dr container array to release (each entry's %data is freed first) +! ************************************************************************************************** + SUBROUTINE nnp_release_dGdr_grp_array(grps) + + TYPE(nnp_dGdr_grp_type), ALLOCATABLE, & + INTENT(INOUT) :: grps(:) + + INTEGER :: s + + IF (.NOT. ALLOCATED(grps)) RETURN + DO s = 1, SIZE(grps) + IF (ALLOCATED(grps(s)%data)) DEALLOCATE (grps(s)%data) + grps(s)%cap = 0 + grps(s)%n_symf = 0 + END DO + DEALLOCATE (grps) + + END SUBROUTINE nnp_release_dGdr_grp_array + +! ************************************************************************************************** +!> \brief Ensure a per-group dG/dr buffer holds n_needed neighbours, growing by +!> 1.5x (with an additive floor) so reallocation amortizes to O(1) over a +!> trajectory. grp%data is allocated on return (cap >= 1), so callers can +!> ASSOCIATE-bind it even for a zero-trip loop. +!> \param grp per-element dGdr group whose %data slab is reallocated if undersized +!> \param n_needed minimum required capacity (third dim of grp%data) +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_grp_grow_dGdr(grp, n_needed) + + TYPE(nnp_dGdr_grp_type), INTENT(INOUT) :: grp + INTEGER, INTENT(IN) :: n_needed + + INTEGER :: new_cap + + IF (ALLOCATED(grp%data) .AND. grp%cap >= n_needed) RETURN + IF (ALLOCATED(grp%data)) DEALLOCATE (grp%data) + new_cap = MAX(MAX(1, n_needed), INT(grp%cap*1.5_dp) + 8) + ALLOCATE (grp%data(3, MAX(1, grp%n_symf), new_cap)) + grp%cap = new_cap + + END SUBROUTINE nnp_grp_grow_dGdr + +! ************************************************************************************************** +!> \brief Ensure a per-group (ind, dist) neighbour buffer holds n_needed entries. +!> Called from inside the linked-cell push loop, so existing entries are +!> preserved on grow via MOVE_ALLOC. 1.5x growth amortizes reallocation to +!> O(1) over a trajectory. +!> \param grp per-species-pair neighbour group whose (ind, dist) buffers grow +!> \param n_needed minimum required entry count +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_neigh_grp_grow(grp, n_needed) + + TYPE(nnp_neigh_grp_type), INTENT(INOUT) :: grp + INTEGER, INTENT(IN) :: n_needed + + INTEGER :: n_old, new_cap + INTEGER, ALLOCATABLE :: new_ind(:) + REAL(KIND=dp), ALLOCATABLE :: new_dist(:, :) + + IF (ALLOCATED(grp%dist) .AND. grp%cap >= n_needed) RETURN + + new_cap = MAX(MAX(8, n_needed), INT(grp%cap*1.5_dp) + 8) + + IF (ALLOCATED(grp%dist)) THEN + n_old = grp%cap + ALLOCATE (new_dist(4, new_cap)) + ALLOCATE (new_ind(new_cap)) + IF (n_old > 0) THEN + new_dist(:, 1:n_old) = grp%dist(:, 1:n_old) + new_ind(1:n_old) = grp%ind(1:n_old) + END IF + CALL MOVE_ALLOC(new_dist, grp%dist) + CALL MOVE_ALLOC(new_ind, grp%ind) + ELSE + ALLOCATE (grp%dist(4, new_cap)) + ALLOCATE (grp%ind(new_cap)) + END IF + grp%cap = new_cap + + END SUBROUTINE nnp_neigh_grp_grow + +! ************************************************************************************************** +!> \brief Ensure the four per-element angular cutoff caches hold at least n_needed +!> entries. These 1D scratch arrays are reused across angular groups within +!> one nnp_calc_acsf call, so the peak is MAX_s(n_ang1) and MAX_s(n_ang2). +!> 1.5x lazy growth. +!> \param workspace per-element workspace whose fc_cache1/dfc_cache1/fc_cache2/dfc_cache2 grow +!> \param n_needed minimum required cache length +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_workspace_grow_caches(workspace, n_needed) + + TYPE(nnp_neighbor_workspace_type), INTENT(INOUT) :: workspace + INTEGER, INTENT(IN) :: n_needed + + INTEGER :: new_cap + + IF (workspace%cache_cap >= n_needed) RETURN + + new_cap = MAX(MAX(8, n_needed), INT(workspace%cache_cap*1.5_dp) + 8) + IF (ALLOCATED(workspace%fc_cache1)) DEALLOCATE (workspace%fc_cache1) + IF (ALLOCATED(workspace%dfc_cache1)) DEALLOCATE (workspace%dfc_cache1) + IF (ALLOCATED(workspace%fc_cache2)) DEALLOCATE (workspace%fc_cache2) + IF (ALLOCATED(workspace%dfc_cache2)) DEALLOCATE (workspace%dfc_cache2) + ALLOCATE (workspace%fc_cache1(new_cap)) + ALLOCATE (workspace%dfc_cache1(new_cap)) + ALLOCATE (workspace%fc_cache2(new_cap)) + ALLOCATE (workspace%dfc_cache2(new_cap)) + workspace%cache_cap = new_cap + + END SUBROUTINE nnp_workspace_grow_caches + +END MODULE nnp_neighbor_interface From cf395cde592db67e22541fd0f93fa6e12897e173 Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Tue, 16 Jun 2026 17:28:38 +0100 Subject: [PATCH 2/8] NNP: ACSF descriptor on the cell-list cache with spline-tabulated radial functions and OpenMP Walk the cell-list cache once per atom to fill the per-element neighbour buffers, tabulate the radial symmetry functions with cubic-Hermite splines (RAD_SPLINE_N knots per group), inline the angular kernel, and parallelise the angular-group loop with OpenMP. Energies and forces match the previous implementation. --- src/nnp_acsf.F | 2005 +++++++++++++++++++++++++++++++----------------- 1 file changed, 1290 insertions(+), 715 deletions(-) diff --git a/src/nnp_acsf.F b/src/nnp_acsf.F index c1a7889a42..91834e554d 100644 --- a/src/nnp_acsf.F +++ b/src/nnp_acsf.F @@ -9,11 +9,10 @@ !> \brief Functionality for atom centered symmetry functions !> for neural network potentials !> \author Christoph Schran (christoph.schran@rub.de) +!> \author Dhruv Sharma (ds2173@cam.ac.uk) !> \date 2020-10-10 ! ************************************************************************************************** MODULE nnp_acsf - USE cell_types, ONLY: cell_type,& - pbc USE cp_log_handling, ONLY: cp_get_default_logger,& cp_logger_get_default_unit_nr,& cp_logger_type @@ -21,24 +20,45 @@ MODULE nnp_acsf dp USE mathconstants, ONLY: pi USE message_passing, ONLY: mp_para_env_type + USE nnp_cell_list, ONLY: nnp_compute_neighbors_cell_list,& + nnp_prepare_cell_list_cache USE nnp_environment_types, ONLY: nnp_acsf_ang_type,& nnp_acsf_rad_type,& nnp_cut_cos,& nnp_cut_tanh,& - nnp_env_get,& - nnp_neighbor_type,& + nnp_symfgrp_type,& nnp_type + USE nnp_neighbor_interface, ONLY: nnp_grp_grow_dGdr,& + nnp_neighbor_interface_prepare,& + nnp_neighbor_interface_reset_neighbor,& + nnp_workspace_grow_caches USE periodic_table, ONLY: get_ptable_info + +!$ USE omp_lib, ONLY: omp_get_max_threads #include "./base/base_uses.f90" IMPLICIT NONE PRIVATE - LOGICAL, PRIVATE, PARAMETER :: debug_this_module = .TRUE. + LOGICAL, PRIVATE, PARAMETER :: debug_this_module = .FALSE. CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'nnp_acsf' + + ! Persistent radial sym-function spline tables, built once on the first + ! nnp_calc_acsf call from the per-(group, sf) eta/rs/cutoff and reused after. + ! Each radial group s holds dense tables spline_y(sf, i)/spline_dy(sf, i) of + ! y(r) = EXP(-eta_sf*(r-rs_sf)^2)*fcut(r) and y'(r). + ! All SFs in a group share the cutoff, so nnp_calc_rad forms the Hermite basis + ! once and streams the sf-first tables. The knot count is set by RAD_SPLINE_N. + + ! Cutoff-equality tolerance for grouping symmetry functions in + ! nnp_init_acsf_groups: SFs whose cutoffs agree to within this absolute + ! tolerance share a group (and a spline grid). + REAL(KIND=dp), PARAMETER, PRIVATE :: cutoff_eq_tol = 1.0e-5_dp + ! Public subroutines *** PUBLIC :: nnp_calc_acsf, & + nnp_prepare_neighbor_cache, & nnp_init_acsf_groups, & nnp_sort_acsf, & nnp_sort_ele, & @@ -48,249 +68,831 @@ CONTAINS ! ************************************************************************************************** !> \brief Calculate atom centered symmetry functions for given atom i -!> \param nnp ... -!> \param i ... -!> \param dsymdxyz ... -!> \param stress ... +!> +!> Per-atom symmetry-function gradients live in the per-element neighbour +!> workspace as sparse per-group arrays: self_dGdr (atom i), dGdr_rad (radial +!> group s), and dGdr_ang_jj / dGdr_ang_kk (angular group s, j- and k-side), +!> each addressed by the neighbour's slot in workspace(ind)%neighbor. No global +!> (3, n_sf, num_atoms) slab is needed: only atoms in atom i's neighbour lists +!> get derivatives. +!> +!> \param nnp NNP environment with persistent neighbour caches populated by +!> nnp_prepare_neighbor_cache (must be called once before the per-atom loop). +!> \param i central-atom index in the global atom ordering; selects +!> nnp%ele_ind(i) for per-element scratch routing. +!> \param calc_forces if .TRUE., populate the per-element dGdr workspaces for caller-side +!> force assembly via nnp_scatter_dgdr_to_forces. +!> \param stress optional per-input-node stress accumulator (only valid when calc_forces). !> \date 2020-10-10 !> \author Christoph Schran (christoph.schran@rub.de) +!> \author Dhruv Sharma (ds2173@cam.ac.uk) ! ************************************************************************************************** - SUBROUTINE nnp_calc_acsf(nnp, i, dsymdxyz, stress) + SUBROUTINE nnp_calc_acsf(nnp, i, calc_forces, stress) TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp INTEGER, INTENT(IN) :: i + LOGICAL, INTENT(IN) :: calc_forces REAL(KIND=dp), DIMENSION(:, :, :), INTENT(INOUT), & - OPTIONAL :: dsymdxyz, stress + OPTIONAL :: stress CHARACTER(len=*), PARAMETER :: routineN = 'nnp_calc_acsf' - INTEGER :: handle, handle_sf, ind, j, jj, k, kk, l, & - m, off, s, sf - REAL(KIND=dp) :: r1, r2, r3 - REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: symtmp - REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: forcetmp - REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :) :: force3tmp - REAL(KIND=dp), DIMENSION(3) :: rvect1, rvect2, rvect3 - TYPE(nnp_neighbor_type) :: neighbor + INTEGER :: handle, handle_nlist, handle_sf, ii, ind, izeta_il, j, k, l, m, n_ang1_s, & + n_ang2_s, n_input_nodes, n_symf_s, nthreads_ang, off, peak, s, sf + LOGICAL :: do_forces, homo_grp + REAL(KIND=dp) :: angular_il, arg_il, costheta_il, cutoff_s, cutoff_sqr, dfcut3_il, & + dfcutdr1_il, dfcutdr2_il, dfcutdr3_il, dgdx_t1, dgdx_t2, dsymdr1_il, dsymdr2_il, & + dsymdr3_il, eta_il, f_il, fcut3_il, ftot_il, g_il, inv_g2_il, lam_il, pref_il, & + pref_lam_il, prefzeta_il, r1, r1_inv, r2, r2_inv, r2sum_il, r3, r3_inv, r3_sqr, rsqr1, & + rsqr2, rsqr3, sym_il, symtmp_il, tanh_il, tmp1_il, tmp2_il, tmp3_il, tmp_il, tmpzeta_il, & + zeta_il + REAL(KIND=dp), DIMENSION(3) :: dcosbase1_il, dcosbase2_il, & + dcosbase3_il, dr1dx_il, dr2dx_il, & + dr3dx_il, f_jj_il, f_kk_il, rvect1, & + rvect2, rvect3 + +! Inlined angular kernel variables (eliminates nnp_calc_ang call overhead +! and intermediate angular_force3tmp array on the serial force path). CALL timeset(routineN, handle) !determine index of atom type ind = nnp%ele_ind(i) + do_forces = calc_forces - ! compute neighbors of atom i - CALL nnp_neighbor_create(nnp, ind, nnp%num_atoms, neighbor) - CALL nnp_compute_neighbors(nnp, neighbor, i) + ! Lazy one-shot build of the per-radial-group spline tables. They depend + ! only on eta/rs/cutoff/cut_type, fixed once nnp_init_acsf_groups and + ! nnp_sort_acsf have run; %spline_built is per-nnp so re-init is safe. + ! Guard the indexed access in case this element has no radial groups. + IF (nnp%rad(ind)%n_symfgrp > 0) THEN + IF (.NOT. nnp%rad(ind)%symfgrp(1)%spline_built) CALL nnp_build_radial_splines(nnp) + END IF - ! Reset y: - nnp%rad(ind)%y = 0.0_dp - nnp%ang(ind)%y = 0.0_dp + ! Persistent per-element workspace bindings. The dGdr_* slabs grow lazily + ! via nnp_grp_grow_dGdr; the fc_cache*/dfc_cache* buffers are bound in an + ! inner ASSOCIATE after nnp_workspace_grow_caches has sized them, so the + ! alias stays valid. + ASSOCIATE (workspace => nnp%neighbor_interface_state%workspace(ind), & + neighbor => nnp%neighbor_interface_state%workspace(ind)%neighbor, & + radial_symtmp => nnp%neighbor_interface_state%workspace(ind)%radial_sym, & + radial_forcetmp => nnp%neighbor_interface_state%workspace(ind)%radial_force, & + angular_symtmp => nnp%neighbor_interface_state%workspace(ind)%angular_sym, & + angular_force3tmp => nnp%neighbor_interface_state%workspace(ind)%angular_force, & + self_dGdr => nnp%neighbor_interface_state%workspace(ind)%self_dGdr) - !calc forces - IF (PRESENT(dsymdxyz)) THEN - !loop over radial sym fnct grps - CALL timeset('nnp_acsf_radial', handle_sf) - DO s = 1, nnp%rad(ind)%n_symfgrp - ALLOCATE (symtmp(nnp%rad(ind)%symfgrp(s)%n_symf)) - ALLOCATE (forcetmp(3, nnp%rad(ind)%symfgrp(s)%n_symf)) - !loop over associated neighbors - DO j = 1, neighbor%n_rad(s) - rvect1 = neighbor%dist_rad(1:3, j, s) - r1 = neighbor%dist_rad(4, j, s) - CALL nnp_calc_rad(nnp, ind, s, rvect1, r1, symtmp, forcetmp) - jj = neighbor%ind_rad(j, s) - DO sf = 1, nnp%rad(ind)%symfgrp(s)%n_symf - m = nnp%rad(ind)%symfgrp(s)%symf(sf) - ! update forces into dsymdxyz - DO l = 1, 3 - dsymdxyz(l, m, i) = dsymdxyz(l, m, i) + forcetmp(l, sf) - dsymdxyz(l, m, jj) = dsymdxyz(l, m, jj) - forcetmp(l, sf) - END DO + n_input_nodes = nnp%neighbor_interface_state%workspace(ind)%n_input_nodes + IF (do_forces) self_dGdr(:, 1:n_input_nodes) = 0.0_dp + + ! Walk the linked-cell candidates directly. The cell-list/neighbour cache + ! is prepared once per force evaluation by nnp_prepare_neighbor_cache, not + ! here, so the per-atom walk stays O(neighbours). + CALL timeset('nnp_acsf_neighbor_fill', handle_nlist) + neighbor%pbc_copies = nnp%cell_list_cache%exact_pbc_copies + CALL nnp_neighbor_interface_reset_neighbor(nnp, ind) + CALL nnp_compute_neighbors_cell_list(nnp, neighbor, i) + CALL timestop(handle_nlist) + + ! Reset y: + nnp%rad(ind)%y = 0.0_dp + nnp%ang(ind)%y = 0.0_dp + + ! Grow the per-element 1D angular cutoff caches to this atom's peak + ! per-group neighbour count, then bind them in an inner ASSOCIATE. + ! MAXVAL of a zero-size array is compiler-defined, so guard each axis. + peak = 0 + IF (SIZE(neighbor%n_ang1) > 0) peak = MAX(peak, MAXVAL(neighbor%n_ang1)) + IF (SIZE(neighbor%n_ang2) > 0) peak = MAX(peak, MAXVAL(neighbor%n_ang2)) + IF (peak > 0) CALL nnp_workspace_grow_caches(workspace, peak) + + ASSOCIATE (fc_cache1 => workspace%fc_cache1, & + dfc_cache1 => workspace%dfc_cache1, & + fc_cache2 => workspace%fc_cache2, & + dfc_cache2 => workspace%dfc_cache2) + + !calc forces + IF (do_forces) THEN + !loop over radial sym fnct grps + CALL timeset('nnp_acsf_radial', handle_sf) + DO s = 1, nnp%rad(ind)%n_symfgrp + n_symf_s = nnp%rad(ind)%symfgrp(s)%n_symf + ! Per-group dense buffer: (3, n_symf_s, cap_s). Grown lazily. + CALL nnp_grp_grow_dGdr(workspace%dGdr_rad(s), neighbor%n_rad(s)) + ASSOCIATE (rad_buf => workspace%dGdr_rad(s)%data) + !loop over associated neighbors + DO j = 1, neighbor%n_rad(s) + rvect1 = neighbor%rad(s)%dist(1:3, j) + r1 = neighbor%rad(s)%dist(4, j) + CALL nnp_calc_rad(nnp, ind, s, rvect1, r1, & + radial_symtmp(1:n_symf_s), & + radial_forcetmp(:, 1:n_symf_s)) + ! Per-group dense write: rad_buf(:, sf, j) holds dG_m/dr_j. + DO sf = 1, n_symf_s + m = nnp%rad(ind)%symfgrp(s)%symf(sf) + self_dGdr(1, m) = self_dGdr(1, m) + radial_forcetmp(1, sf) + self_dGdr(2, m) = self_dGdr(2, m) + radial_forcetmp(2, sf) + self_dGdr(3, m) = self_dGdr(3, m) + radial_forcetmp(3, sf) + rad_buf(1, sf, j) = -radial_forcetmp(1, sf) + rad_buf(2, sf, j) = -radial_forcetmp(2, sf) + rad_buf(3, sf, j) = -radial_forcetmp(3, sf) + IF (PRESENT(stress)) THEN + DO l = 1, 3 + stress(:, l, m) = stress(:, l, m) + rvect1(:)*radial_forcetmp(l, sf) + END DO + END IF + nnp%rad(ind)%y(m) = nnp%rad(ind)%y(m) + radial_symtmp(sf) + END DO + END DO + END ASSOCIATE + END DO + CALL timestop(handle_sf) + + !loop over angular sym fnct grps + CALL timeset('nnp_acsf_angular', handle_sf) + off = nnp%n_rad(ind) + + ! OpenMP over the angular group index s, taken only with >1 thread + ! and >1 group. Angular groups partition the input-node index m + ! disjointly (nnp_sort_acsf), so self_dGdr, stress and y are written + ! without races. The serial path below is identical. + nthreads_ang = 1 +!$ nthreads_ang = omp_get_max_threads() + IF (nthreads_ang > 1 .AND. nnp%ang(ind)%n_symfgrp > 1) THEN IF (PRESENT(stress)) THEN - DO l = 1, 3 - stress(:, l, m) = stress(:, l, m) + rvect1(:)*forcetmp(l, sf) + CALL nnp_acsf_angular_loop_omp(nnp, ind, self_dGdr, off, stress) + ELSE + CALL nnp_acsf_angular_loop_omp(nnp, ind, self_dGdr, off) + END IF + ELSE + DO s = 1, nnp%ang(ind)%n_symfgrp + cutoff_s = nnp%ang(ind)%symfgrp(s)%cutoff + cutoff_sqr = cutoff_s*cutoff_s + n_symf_s = nnp%ang(ind)%symfgrp(s)%n_symf + n_ang1_s = neighbor%n_ang1(s) + homo_grp = (nnp%ang(ind)%symfgrp(s)%ele(1) == nnp%ang(ind)%symfgrp(s)%ele(2)) + + ! Grow per-group dense buffers. jj is always indexed in ang1. + ! kk is indexed in ang1 for homo groups and in ang2 for hetero. + CALL nnp_grp_grow_dGdr(workspace%dGdr_ang_jj(s), n_ang1_s) + IF (homo_grp) THEN + CALL nnp_grp_grow_dGdr(workspace%dGdr_ang_kk(s), n_ang1_s) + n_ang2_s = 0 + ELSE + n_ang2_s = neighbor%n_ang2(s) + CALL nnp_grp_grow_dGdr(workspace%dGdr_ang_kk(s), n_ang2_s) + END IF + + ! Per-group reset. Triplets accumulate into the same (sf, j) slot + ! across multiple k partners, so we MUST zero before the triplet loop. + IF (n_ang1_s > 0) workspace%dGdr_ang_jj(s)%data(:, 1:n_symf_s, 1:n_ang1_s) = 0.0_dp + IF (homo_grp) THEN + IF (n_ang1_s > 0) workspace%dGdr_ang_kk(s)%data(:, 1:n_symf_s, 1:n_ang1_s) = 0.0_dp + ELSE + IF (n_ang2_s > 0) workspace%dGdr_ang_kk(s)%data(:, 1:n_symf_s, 1:n_ang2_s) = 0.0_dp + END IF + + ! Precompute cutoff values and derivatives for ang1 neighbors + CALL nnp_fill_fc_dfc_cache(neighbor%ang1(s)%dist, n_ang1_s, & + nnp%cut_type, cutoff_s, fc_cache1, dfc_cache1) + + ! Inlined angular kernel: compute + scatter fused. The (j,k) + ! geometry is computed once, then the SF loop scatters sym + ! values and forces directly into self_dGdr / jj_buf / kk_buf, + ! keeping per-triple scalars in registers. Identical to nnp_calc_ang. + IF (homo_grp) THEN + ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & + kk_buf => workspace%dGdr_ang_kk(s)%data, & + grp_il => nnp%ang(ind)%symfgrp(s)) + DO j = 1, n_ang1_s + rvect1 = neighbor%ang1(s)%dist(1:3, j) + r1 = neighbor%ang1(s)%dist(4, j) + DO k = j + 1, n_ang1_s + rvect2 = neighbor%ang1(s)%dist(1:3, k) + r2 = neighbor%ang1(s)%dist(4, k) + rvect3(1) = rvect2(1) - rvect1(1) + rvect3(2) = rvect2(2) - rvect1(2) + rvect3(3) = rvect2(3) - rvect1(3) + r3_sqr = rvect3(1)*rvect3(1) + rvect3(2)*rvect3(2) + rvect3(3)*rvect3(3) + IF (r3_sqr < cutoff_sqr) THEN + r3 = SQRT(r3_sqr) + + ! -- per-triple geometry (once) -- + rsqr1 = r1*r1; rsqr2 = r2*r2; rsqr3 = r3*r3 + r2sum_il = rsqr1 + rsqr2 + rsqr3 + f_il = rsqr3 - rsqr1 - rsqr2 + g_il = -2.0_dp*r1*r2 + costheta_il = f_il/g_il + + SELECT CASE (nnp%cut_type) + CASE (nnp_cut_cos) + arg_il = pi*r3/cutoff_s + fcut3_il = 0.5_dp*(COS(arg_il) + 1.0_dp) + dfcut3_il = -0.5_dp*SIN(arg_il)*(pi/cutoff_s) + CASE (nnp_cut_tanh) + tanh_il = TANH(1.0_dp - r3/cutoff_s) + fcut3_il = tanh_il**3 + dfcut3_il = (-3.0_dp/cutoff_s)*(tanh_il**2 - tanh_il**4) + CASE DEFAULT + CPABORT("NNP| Cutoff function unknown") + END SELECT + + ftot_il = fc_cache1(j)*fc_cache1(k)*fcut3_il + dfcutdr1_il = dfc_cache1(j)*fc_cache1(k)*fcut3_il + dfcutdr2_il = fc_cache1(j)*dfc_cache1(k)*fcut3_il + dfcutdr3_il = fc_cache1(j)*fc_cache1(k)*dfcut3_il + + r1_inv = 1.0_dp/r1; r2_inv = 1.0_dp/r2; r3_inv = 1.0_dp/r3 + dr1dx_il(:) = rvect1(:)*r1_inv + dr2dx_il(:) = rvect2(:)*r2_inv + dr3dx_il(:) = rvect3(:)*r3_inv + + inv_g2_il = 1.0_dp/(g_il*g_il) + DO ii = 1, 3 + dgdx_t1 = 2.0_dp*r2*dr1dx_il(ii) + dgdx_t2 = 2.0_dp*r1*dr2dx_il(ii) + dcosbase1_il(ii) = -2.0_dp*(rvect1(ii) + rvect2(ii))*g_il & + - f_il*(-(dgdx_t1 + dgdx_t2)) + dcosbase2_il(ii) = 2.0_dp*(rvect3(ii) + rvect1(ii))*g_il & + - f_il*dgdx_t1 + dcosbase3_il(ii) = 2.0_dp*(rvect2(ii) - rvect3(ii))*g_il & + - f_il*dgdx_t2 + END DO + + ! -- fused SF loop: compute + direct scatter -- + DO sf = 1, n_symf_s + m = off + grp_il%symf(sf) + lam_il = grp_il%pack_lam(sf) + zeta_il = grp_il%pack_zeta(sf) + eta_il = grp_il%pack_eta(sf) + prefzeta_il = grp_il%pack_prefzeta(sf) + + tmp_il = 1.0_dp + lam_il*costheta_il + IF (tmp_il <= 0.0_dp) THEN + tmpzeta_il = 0.0_dp + angular_il = 0.0_dp + ELSE + IF (grp_il%pack_use_int_zeta(sf)) THEN + izeta_il = grp_il%pack_izeta(sf) + tmpzeta_il = tmp_il**(izeta_il - 1) + ELSE + tmpzeta_il = tmp_il**(zeta_il - 1.0_dp) + END IF + angular_il = tmpzeta_il*tmp_il + END IF + + symtmp_il = EXP(-eta_il*r2sum_il) + sym_il = prefzeta_il*angular_il*symtmp_il*ftot_il + nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + sym_il + + pref_lam_il = zeta_il*tmpzeta_il*lam_il*inv_g2_il + tmp_il = -2.0_dp*symtmp_il*eta_il + dsymdr1_il = tmp_il*r1 + dsymdr2_il = tmp_il*r2 + dsymdr3_il = tmp_il*r3 + + pref_il = prefzeta_il*symtmp_il*ftot_il + tmp1_il = prefzeta_il*angular_il*(ftot_il*dsymdr1_il + dfcutdr1_il*symtmp_il) + tmp2_il = prefzeta_il*angular_il*(ftot_il*dsymdr2_il + dfcutdr2_il*symtmp_il) + tmp3_il = prefzeta_il*angular_il*(ftot_il*dsymdr3_il + dfcutdr3_il*symtmp_il) + + DO ii = 1, 3 + f_jj_il(ii) = pref_il*pref_lam_il*dcosbase2_il(ii) & + - tmp1_il*dr1dx_il(ii) + tmp3_il*dr3dx_il(ii) + f_kk_il(ii) = pref_il*pref_lam_il*dcosbase3_il(ii) & + - tmp2_il*dr2dx_il(ii) - tmp3_il*dr3dx_il(ii) + self_dGdr(ii, m) = self_dGdr(ii, m) & + + pref_il*pref_lam_il*dcosbase1_il(ii) & + + tmp1_il*dr1dx_il(ii) + tmp2_il*dr2dx_il(ii) + jj_buf(ii, sf, j) = jj_buf(ii, sf, j) + f_jj_il(ii) + kk_buf(ii, sf, k) = kk_buf(ii, sf, k) + f_kk_il(ii) + END DO + IF (PRESENT(stress)) THEN + DO l = 1, 3 + stress(:, l, m) = stress(:, l, m) & + - rvect1(:)*f_jj_il(l) - rvect2(:)*f_kk_il(l) + END DO + END IF + END DO + + END IF + END DO + END DO + END ASSOCIATE + ELSE + ! Precompute cutoff values for ang2 neighbors (different elements) + CALL nnp_fill_fc_dfc_cache(neighbor%ang2(s)%dist, n_ang2_s, & + nnp%cut_type, cutoff_s, fc_cache2, dfc_cache2) + + ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & + kk_buf => workspace%dGdr_ang_kk(s)%data, & + grp_il => nnp%ang(ind)%symfgrp(s)) + DO j = 1, n_ang1_s + rvect1 = neighbor%ang1(s)%dist(1:3, j) + r1 = neighbor%ang1(s)%dist(4, j) + DO k = 1, n_ang2_s + rvect2 = neighbor%ang2(s)%dist(1:3, k) + r2 = neighbor%ang2(s)%dist(4, k) + rvect3(1) = rvect2(1) - rvect1(1) + rvect3(2) = rvect2(2) - rvect1(2) + rvect3(3) = rvect2(3) - rvect1(3) + r3_sqr = rvect3(1)*rvect3(1) + rvect3(2)*rvect3(2) + rvect3(3)*rvect3(3) + IF (r3_sqr < cutoff_sqr) THEN + r3 = SQRT(r3_sqr) + + ! -- per-triple geometry (once) -- + rsqr1 = r1*r1; rsqr2 = r2*r2; rsqr3 = r3*r3 + r2sum_il = rsqr1 + rsqr2 + rsqr3 + f_il = rsqr3 - rsqr1 - rsqr2 + g_il = -2.0_dp*r1*r2 + costheta_il = f_il/g_il + + SELECT CASE (nnp%cut_type) + CASE (nnp_cut_cos) + arg_il = pi*r3/cutoff_s + fcut3_il = 0.5_dp*(COS(arg_il) + 1.0_dp) + dfcut3_il = -0.5_dp*SIN(arg_il)*(pi/cutoff_s) + CASE (nnp_cut_tanh) + tanh_il = TANH(1.0_dp - r3/cutoff_s) + fcut3_il = tanh_il**3 + dfcut3_il = (-3.0_dp/cutoff_s)*(tanh_il**2 - tanh_il**4) + CASE DEFAULT + CPABORT("NNP| Cutoff function unknown") + END SELECT + + ftot_il = fc_cache1(j)*fc_cache2(k)*fcut3_il + dfcutdr1_il = dfc_cache1(j)*fc_cache2(k)*fcut3_il + dfcutdr2_il = fc_cache1(j)*dfc_cache2(k)*fcut3_il + dfcutdr3_il = fc_cache1(j)*fc_cache2(k)*dfcut3_il + + r1_inv = 1.0_dp/r1; r2_inv = 1.0_dp/r2; r3_inv = 1.0_dp/r3 + dr1dx_il(:) = rvect1(:)*r1_inv + dr2dx_il(:) = rvect2(:)*r2_inv + dr3dx_il(:) = rvect3(:)*r3_inv + + inv_g2_il = 1.0_dp/(g_il*g_il) + DO ii = 1, 3 + dgdx_t1 = 2.0_dp*r2*dr1dx_il(ii) + dgdx_t2 = 2.0_dp*r1*dr2dx_il(ii) + dcosbase1_il(ii) = -2.0_dp*(rvect1(ii) + rvect2(ii))*g_il & + - f_il*(-(dgdx_t1 + dgdx_t2)) + dcosbase2_il(ii) = 2.0_dp*(rvect3(ii) + rvect1(ii))*g_il & + - f_il*dgdx_t1 + dcosbase3_il(ii) = 2.0_dp*(rvect2(ii) - rvect3(ii))*g_il & + - f_il*dgdx_t2 + END DO + + ! -- fused SF loop: compute + direct scatter -- + DO sf = 1, n_symf_s + m = off + grp_il%symf(sf) + lam_il = grp_il%pack_lam(sf) + zeta_il = grp_il%pack_zeta(sf) + eta_il = grp_il%pack_eta(sf) + prefzeta_il = grp_il%pack_prefzeta(sf) + + tmp_il = 1.0_dp + lam_il*costheta_il + IF (tmp_il <= 0.0_dp) THEN + tmpzeta_il = 0.0_dp + angular_il = 0.0_dp + ELSE + IF (grp_il%pack_use_int_zeta(sf)) THEN + izeta_il = grp_il%pack_izeta(sf) + tmpzeta_il = tmp_il**(izeta_il - 1) + ELSE + tmpzeta_il = tmp_il**(zeta_il - 1.0_dp) + END IF + angular_il = tmpzeta_il*tmp_il + END IF + + symtmp_il = EXP(-eta_il*r2sum_il) + sym_il = prefzeta_il*angular_il*symtmp_il*ftot_il + nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + sym_il + + pref_lam_il = zeta_il*tmpzeta_il*lam_il*inv_g2_il + tmp_il = -2.0_dp*symtmp_il*eta_il + dsymdr1_il = tmp_il*r1 + dsymdr2_il = tmp_il*r2 + dsymdr3_il = tmp_il*r3 + + pref_il = prefzeta_il*symtmp_il*ftot_il + tmp1_il = prefzeta_il*angular_il*(ftot_il*dsymdr1_il + dfcutdr1_il*symtmp_il) + tmp2_il = prefzeta_il*angular_il*(ftot_il*dsymdr2_il + dfcutdr2_il*symtmp_il) + tmp3_il = prefzeta_il*angular_il*(ftot_il*dsymdr3_il + dfcutdr3_il*symtmp_il) + + DO ii = 1, 3 + f_jj_il(ii) = pref_il*pref_lam_il*dcosbase2_il(ii) & + - tmp1_il*dr1dx_il(ii) + tmp3_il*dr3dx_il(ii) + f_kk_il(ii) = pref_il*pref_lam_il*dcosbase3_il(ii) & + - tmp2_il*dr2dx_il(ii) - tmp3_il*dr3dx_il(ii) + self_dGdr(ii, m) = self_dGdr(ii, m) & + + pref_il*pref_lam_il*dcosbase1_il(ii) & + + tmp1_il*dr1dx_il(ii) + tmp2_il*dr2dx_il(ii) + jj_buf(ii, sf, j) = jj_buf(ii, sf, j) + f_jj_il(ii) + kk_buf(ii, sf, k) = kk_buf(ii, sf, k) + f_kk_il(ii) + END DO + IF (PRESENT(stress)) THEN + DO l = 1, 3 + stress(:, l, m) = stress(:, l, m) & + - rvect1(:)*f_jj_il(l) - rvect2(:)*f_kk_il(l) + END DO + END IF + END DO + + END IF + END DO + END DO + END ASSOCIATE + END IF + END DO + END IF + CALL timestop(handle_sf) + ELSE + !loop over radial sym fnct grps + CALL timeset('nnp_acsf_radial', handle_sf) + DO s = 1, nnp%rad(ind)%n_symfgrp + !loop over associated neighbors + DO j = 1, neighbor%n_rad(s) + rvect1 = neighbor%rad(s)%dist(1:3, j) + r1 = neighbor%rad(s)%dist(4, j) + CALL nnp_calc_rad(nnp, ind, s, rvect1, r1, radial_symtmp(1:nnp%rad(ind)%symfgrp(s)%n_symf)) + DO sf = 1, nnp%rad(ind)%symfgrp(s)%n_symf + m = nnp%rad(ind)%symfgrp(s)%symf(sf) + nnp%rad(ind)%y(m) = nnp%rad(ind)%y(m) + radial_symtmp(sf) + END DO + END DO + END DO + CALL timestop(handle_sf) + + !loop over angular sym fnct grps + CALL timeset('nnp_acsf_angular', handle_sf) + off = nnp%n_rad(ind) + DO s = 1, nnp%ang(ind)%n_symfgrp + cutoff_s = nnp%ang(ind)%symfgrp(s)%cutoff + cutoff_sqr = cutoff_s*cutoff_s + n_symf_s = nnp%ang(ind)%symfgrp(s)%n_symf + n_ang1_s = neighbor%n_ang1(s) + + ! Precompute cutoff values for ang1 neighbors (no derivatives needed) + CALL nnp_fill_fc_cache(neighbor%ang1(s)%dist, n_ang1_s, & + nnp%cut_type, cutoff_s, fc_cache1) + + IF (nnp%ang(ind)%symfgrp(s)%ele(1) == nnp%ang(ind)%symfgrp(s)%ele(2)) THEN + DO j = 1, n_ang1_s + rvect1 = neighbor%ang1(s)%dist(1:3, j) + r1 = neighbor%ang1(s)%dist(4, j) + DO k = j + 1, n_ang1_s + rvect2 = neighbor%ang1(s)%dist(1:3, k) + r2 = neighbor%ang1(s)%dist(4, k) + rvect3(1) = rvect2(1) - rvect1(1) + rvect3(2) = rvect2(2) - rvect1(2) + rvect3(3) = rvect2(3) - rvect1(3) + r3_sqr = rvect3(1)*rvect3(1) + rvect3(2)*rvect3(2) + rvect3(3)*rvect3(3) + IF (r3_sqr < cutoff_sqr) THEN + r3 = SQRT(r3_sqr) + CALL nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, r1, r2, r3, & + fc_cache1(j), 0.0_dp, fc_cache1(k), 0.0_dp, & + angular_symtmp(1:n_symf_s)) + DO sf = 1, n_symf_s + m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) + nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + angular_symtmp(sf) + END DO + END IF + END DO + END DO + ELSE + ! Precompute cutoff values for ang2 neighbors + n_ang2_s = neighbor%n_ang2(s) + CALL nnp_fill_fc_cache(neighbor%ang2(s)%dist, n_ang2_s, & + nnp%cut_type, cutoff_s, fc_cache2) + + DO j = 1, n_ang1_s + rvect1 = neighbor%ang1(s)%dist(1:3, j) + r1 = neighbor%ang1(s)%dist(4, j) + DO k = 1, n_ang2_s + rvect2 = neighbor%ang2(s)%dist(1:3, k) + r2 = neighbor%ang2(s)%dist(4, k) + rvect3(1) = rvect2(1) - rvect1(1) + rvect3(2) = rvect2(2) - rvect1(2) + rvect3(3) = rvect2(3) - rvect1(3) + r3_sqr = rvect3(1)*rvect3(1) + rvect3(2)*rvect3(2) + rvect3(3)*rvect3(3) + IF (r3_sqr < cutoff_sqr) THEN + r3 = SQRT(r3_sqr) + CALL nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, r1, r2, r3, & + fc_cache1(j), 0.0_dp, fc_cache2(k), 0.0_dp, & + angular_symtmp(1:n_symf_s)) + DO sf = 1, n_symf_s + m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) + nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + angular_symtmp(sf) + END DO + END IF + END DO END DO END IF - nnp%rad(ind)%y(m) = nnp%rad(ind)%y(m) + symtmp(sf) - END DO - END DO - DEALLOCATE (symtmp) - DEALLOCATE (forcetmp) - END DO - CALL timestop(handle_sf) - !loop over angular sym fnct grps - CALL timeset('nnp_acsf_angular', handle_sf) - off = nnp%n_rad(ind) - DO s = 1, nnp%ang(ind)%n_symfgrp - ALLOCATE (symtmp(nnp%ang(ind)%symfgrp(s)%n_symf)) - ALLOCATE (force3tmp(3, 3, nnp%ang(ind)%symfgrp(s)%n_symf)) - !loop over associated neighbors - IF (nnp%ang(ind)%symfgrp(s)%ele(1) == nnp%ang(ind)%symfgrp(s)%ele(2)) THEN - DO j = 1, neighbor%n_ang1(s) - rvect1 = neighbor%dist_ang1(1:3, j, s) - r1 = neighbor%dist_ang1(4, j, s) - DO k = j + 1, neighbor%n_ang1(s) - rvect2 = neighbor%dist_ang1(1:3, k, s) - r2 = neighbor%dist_ang1(4, k, s) - rvect3 = rvect2 - rvect1 - r3 = NORM2(rvect3(:)) - IF (r3 < nnp%ang(ind)%symfgrp(s)%cutoff) THEN - jj = neighbor%ind_ang1(j, s) - kk = neighbor%ind_ang1(k, s) - CALL nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, & - r1, r2, r3, symtmp, force3tmp) - DO sf = 1, nnp%ang(ind)%symfgrp(s)%n_symf - m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) - ! update forces into dsymdxy - DO l = 1, 3 - dsymdxyz(l, m, i) = dsymdxyz(l, m, i) & - + force3tmp(l, 1, sf) - dsymdxyz(l, m, jj) = dsymdxyz(l, m, jj) & - + force3tmp(l, 2, sf) - dsymdxyz(l, m, kk) = dsymdxyz(l, m, kk) & - + force3tmp(l, 3, sf) - END DO - IF (PRESENT(stress)) THEN - DO l = 1, 3 - stress(:, l, m) = stress(:, l, m) - rvect1(:)*force3tmp(l, 2, sf) - stress(:, l, m) = stress(:, l, m) - rvect2(:)*force3tmp(l, 3, sf) - END DO - END IF - nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + symtmp(sf) - END DO - END IF - END DO - END DO - ELSE - DO j = 1, neighbor%n_ang1(s) - rvect1 = neighbor%dist_ang1(1:3, j, s) - r1 = neighbor%dist_ang1(4, j, s) - DO k = 1, neighbor%n_ang2(s) - rvect2 = neighbor%dist_ang2(1:3, k, s) - r2 = neighbor%dist_ang2(4, k, s) - rvect3 = rvect2 - rvect1 - r3 = NORM2(rvect3(:)) - IF (r3 < nnp%ang(ind)%symfgrp(s)%cutoff) THEN - jj = neighbor%ind_ang1(j, s) - kk = neighbor%ind_ang1(k, s) - CALL nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, & - r1, r2, r3, symtmp, force3tmp) - !loop over associated sym fncts - DO sf = 1, nnp%ang(ind)%symfgrp(s)%n_symf - m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) - jj = neighbor%ind_ang1(j, s) - kk = neighbor%ind_ang2(k, s) - ! update forces into dsymdxy - DO l = 1, 3 - dsymdxyz(l, m, i) = dsymdxyz(l, m, i) & - + force3tmp(l, 1, sf) - dsymdxyz(l, m, jj) = dsymdxyz(l, m, jj) & - + force3tmp(l, 2, sf) - dsymdxyz(l, m, kk) = dsymdxyz(l, m, kk) & - + force3tmp(l, 3, sf) - END DO - IF (PRESENT(stress)) THEN - DO l = 1, 3 - stress(:, l, m) = stress(:, l, m) - rvect1(:)*force3tmp(l, 2, sf) - stress(:, l, m) = stress(:, l, m) - rvect2(:)*force3tmp(l, 3, sf) - END DO - END IF - nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + symtmp(sf) - END DO - END IF - END DO END DO + CALL timestop(handle_sf) END IF - DEALLOCATE (symtmp) - DEALLOCATE (force3tmp) - END DO - CALL timestop(handle_sf) - !no forces - ELSE - !loop over radial sym fnct grps - CALL timeset('nnp_acsf_radial', handle_sf) - DO s = 1, nnp%rad(ind)%n_symfgrp - ALLOCATE (symtmp(nnp%rad(ind)%symfgrp(s)%n_symf)) - !loop over associated neighbors - DO j = 1, neighbor%n_rad(s) - rvect1 = neighbor%dist_rad(1:3, j, s) - r1 = neighbor%dist_rad(4, j, s) - CALL nnp_calc_rad(nnp, ind, s, rvect1, r1, symtmp) - DO sf = 1, nnp%rad(ind)%symfgrp(s)%n_symf - m = nnp%rad(ind)%symfgrp(s)%symf(sf) - nnp%rad(ind)%y(m) = nnp%rad(ind)%y(m) + symtmp(sf) - END DO - END DO - DEALLOCATE (symtmp) - END DO - CALL timestop(handle_sf) - !loop over angular sym fnct grps - CALL timeset('nnp_acsf_angular', handle_sf) - off = nnp%n_rad(ind) - DO s = 1, nnp%ang(ind)%n_symfgrp - ALLOCATE (symtmp(nnp%ang(ind)%symfgrp(s)%n_symf)) - !loop over associated neighbors - IF (nnp%ang(ind)%symfgrp(s)%ele(1) == nnp%ang(ind)%symfgrp(s)%ele(2)) THEN - DO j = 1, neighbor%n_ang1(s) - rvect1 = neighbor%dist_ang1(1:3, j, s) - r1 = neighbor%dist_ang1(4, j, s) - DO k = j + 1, neighbor%n_ang1(s) - rvect2 = neighbor%dist_ang1(1:3, k, s) - r2 = neighbor%dist_ang1(4, k, s) - rvect3 = rvect2 - rvect1 - r3 = NORM2(rvect3(:)) - IF (r3 < nnp%ang(ind)%symfgrp(s)%cutoff) THEN - CALL nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, r1, r2, r3, symtmp) - DO sf = 1, nnp%ang(ind)%symfgrp(s)%n_symf - m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) - nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + symtmp(sf) - END DO - END IF - END DO - END DO - ELSE - DO j = 1, neighbor%n_ang1(s) - rvect1 = neighbor%dist_ang1(1:3, j, s) - r1 = neighbor%dist_ang1(4, j, s) - DO k = 1, neighbor%n_ang2(s) - rvect2 = neighbor%dist_ang2(1:3, k, s) - r2 = neighbor%dist_ang2(4, k, s) - rvect3 = rvect2 - rvect1 - r3 = NORM2(rvect3(:)) - IF (r3 < nnp%ang(ind)%symfgrp(s)%cutoff) THEN - CALL nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, r1, r2, r3, symtmp) - !loop over associated sym fncts - DO sf = 1, nnp%ang(ind)%symfgrp(s)%n_symf - m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) - nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + symtmp(sf) - END DO - END IF - END DO - END DO - END IF - DEALLOCATE (symtmp) - END DO - CALL timestop(handle_sf) - END IF + + END ASSOCIATE + + ! fc_cache1/2 and dfc_cache1/2 are persistent workspace; nothing to deallocate here. + + END ASSOCIATE !check extrapolation CALL nnp_check_extrapolation(nnp, ind) - IF (PRESENT(dsymdxyz)) THEN - IF (PRESENT(stress)) THEN - CALL nnp_scale_acsf(nnp, ind, dsymdxyz, stress) - ELSE - CALL nnp_scale_acsf(nnp, ind, dsymdxyz) - END IF + IF (PRESENT(stress)) THEN + CALL nnp_scale_acsf(nnp, ind, do_forces, stress) ELSE - CALL nnp_scale_acsf(nnp, ind) + CALL nnp_scale_acsf(nnp, ind, do_forces) END IF - CALL nnp_neighbor_release(neighbor) CALL timestop(handle) END SUBROUTINE nnp_calc_acsf +! ************************************************************************************************** +!> \brief Fill the per-neighbour fc and dfc cutoff-function caches used by the +!> force branch of the angular ACSF kernel. +!> \param dist (4, :) neighbour array; column 4 holds the scalar distance +!> \param n number of neighbors to process +!> \param cut_type cutoff function selector (nnp_cut_cos / nnp_cut_tanh) +!> \param cutoff_s per-group cutoff radius +!> \param fc_cache output fc values, sized >= n +!> \param dfc_cache output dfc values, sized >= n +! ************************************************************************************************** + PURE SUBROUTINE nnp_fill_fc_dfc_cache(dist, n, cut_type, cutoff_s, fc_cache, dfc_cache) + REAL(KIND=dp), DIMENSION(:, :), INTENT(IN) :: dist + INTEGER, INTENT(IN) :: n, cut_type + REAL(KIND=dp), INTENT(IN) :: cutoff_s + REAL(KIND=dp), DIMENSION(:), INTENT(OUT) :: fc_cache, dfc_cache + + INTEGER :: j + REAL(KIND=dp) :: arg_tmp, r_tmp, tanh_tmp + + DO j = 1, n + r_tmp = dist(4, j) + SELECT CASE (cut_type) + CASE (nnp_cut_cos) + arg_tmp = pi*r_tmp/cutoff_s + fc_cache(j) = 0.5_dp*(COS(arg_tmp) + 1.0_dp) + dfc_cache(j) = -0.5_dp*SIN(arg_tmp)*(pi/cutoff_s) + CASE (nnp_cut_tanh) + tanh_tmp = TANH(1.0_dp - r_tmp/cutoff_s) + fc_cache(j) = tanh_tmp**3 + dfc_cache(j) = (-3.0_dp/cutoff_s)*(tanh_tmp**2 - tanh_tmp**4) + END SELECT + END DO + + END SUBROUTINE nnp_fill_fc_dfc_cache + +! ************************************************************************************************** +!> \brief Fill the per-neighbour fc cutoff-function cache for the sym-only +!> (no-forces) branch of the angular ACSF kernel. Derivatives are not needed. +!> \param dist (4, :) neighbour array; column 4 holds the scalar distance +!> \param n number of neighbors to process +!> \param cut_type cutoff function selector (nnp_cut_cos / nnp_cut_tanh) +!> \param cutoff_s per-group cutoff radius +!> \param fc_cache output fc values, sized >= n +! ************************************************************************************************** + PURE SUBROUTINE nnp_fill_fc_cache(dist, n, cut_type, cutoff_s, fc_cache) + REAL(KIND=dp), DIMENSION(:, :), INTENT(IN) :: dist + INTEGER, INTENT(IN) :: n, cut_type + REAL(KIND=dp), INTENT(IN) :: cutoff_s + REAL(KIND=dp), DIMENSION(:), INTENT(OUT) :: fc_cache + + INTEGER :: j + REAL(KIND=dp) :: r_tmp, tanh_tmp + + DO j = 1, n + r_tmp = dist(4, j) + SELECT CASE (cut_type) + CASE (nnp_cut_cos) + fc_cache(j) = 0.5_dp*(COS(pi*r_tmp/cutoff_s) + 1.0_dp) + CASE (nnp_cut_tanh) + tanh_tmp = TANH(1.0_dp - r_tmp/cutoff_s) + fc_cache(j) = tanh_tmp**3 + END SELECT + END DO + + END SUBROUTINE nnp_fill_fc_cache + +! ************************************************************************************************** +!> \brief OpenMP parallelization of the angular SF group loop over s. Groups +!> partition the angular SF indices disjointly, so per-group writes into +!> self_dGdr / stress / nnp%ang%y and the workspace dGdr_ang accumulators +!> are race-free. Per-thread scratch is PRIVATE automatic arrays; buffer +!> growth and zero-init run in a serial pre-pass so the parallel region +!> never touches ALLOCATABLE state. +!> \param nnp ... +!> \param ind ... +!> \param self_dGdr ... +!> \param off ... +!> \param stress ... +! ************************************************************************************************** + SUBROUTINE nnp_acsf_angular_loop_omp(nnp, ind, self_dGdr, off, stress) + TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp + INTEGER, INTENT(IN) :: ind + REAL(KIND=dp), DIMENSION(:, :), INTENT(INOUT) :: self_dGdr + INTEGER, INTENT(IN) :: off + REAL(KIND=dp), DIMENSION(:, :, :), INTENT(INOUT), & + OPTIONAL :: stress + + INTEGER :: cache_cap_loc, j, k, l, m, & + max_ang_symf_loc, n_ang1_s, n_ang2_s, & + n_symf_s, s, sf + LOGICAL :: homo_grp + REAL(KIND=dp) :: cutoff_s, cutoff_sqr, r1, r2, r3, r3_sqr + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: dfc_c1_loc, dfc_c2_loc, fc_c1_loc, & + fc_c2_loc, sym_loc + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :) :: force_loc + REAL(KIND=dp), DIMENSION(3) :: rvect1, rvect2, rvect3 + +! Per-thread automatic scratch (see PRIVATE clause below). Sizes +! are pulled from the pre-sized workspace so they match the peak +! usage that the serial kernel would allocate into the workspace +! %fc_cache*/angular_* slabs. + + ASSOCIATE (workspace => nnp%neighbor_interface_state%workspace(ind), & + neighbor => nnp%neighbor_interface_state%workspace(ind)%neighbor) + + cache_cap_loc = MAX(1, workspace%cache_cap) + max_ang_symf_loc = MAX(1, workspace%max_ang_symf) + + ! Serial pre-pass: grow per-group accumulator slabs and zero them + ! out. Doing this outside the parallel region guarantees no thread + ! ever touches ALLOCATABLE components of nnp_dGdr_grp_type. + DO s = 1, nnp%ang(ind)%n_symfgrp + n_symf_s = nnp%ang(ind)%symfgrp(s)%n_symf + n_ang1_s = neighbor%n_ang1(s) + homo_grp = (nnp%ang(ind)%symfgrp(s)%ele(1) == nnp%ang(ind)%symfgrp(s)%ele(2)) + CALL nnp_grp_grow_dGdr(workspace%dGdr_ang_jj(s), n_ang1_s) + IF (homo_grp) THEN + CALL nnp_grp_grow_dGdr(workspace%dGdr_ang_kk(s), n_ang1_s) + n_ang2_s = 0 + ELSE + n_ang2_s = neighbor%n_ang2(s) + CALL nnp_grp_grow_dGdr(workspace%dGdr_ang_kk(s), n_ang2_s) + END IF + IF (n_ang1_s > 0) workspace%dGdr_ang_jj(s)%data(:, 1:n_symf_s, 1:n_ang1_s) = 0.0_dp + IF (homo_grp) THEN + IF (n_ang1_s > 0) workspace%dGdr_ang_kk(s)%data(:, 1:n_symf_s, 1:n_ang1_s) = 0.0_dp + ELSE + IF (n_ang2_s > 0) workspace%dGdr_ang_kk(s)%data(:, 1:n_symf_s, 1:n_ang2_s) = 0.0_dp + END IF + END DO + + ! Each thread allocates its own PRIVATE scratch inside the parallel + ! region: an ALLOCATABLE listed as PRIVATE enters unallocated per + ! thread, so the ALLOCATE below gives each thread an independent slab. + ! DEFAULT(SHARED) (not NONE) keeps the OPTIONAL `stress` dummy out of + ! the data-sharing clauses; the PRESENT() guard alone gates its use. +!$OMP PARALLEL DEFAULT(SHARED) & +!$OMP PRIVATE(s, j, k, sf, m, l, n_symf_s, n_ang1_s, n_ang2_s, & +!$OMP r1, r2, r3, r3_sqr, cutoff_s, cutoff_sqr, homo_grp, & +!$OMP rvect1, rvect2, rvect3, & +!$OMP fc_c1_loc, dfc_c1_loc, fc_c2_loc, dfc_c2_loc, & +!$OMP sym_loc, force_loc) + ALLOCATE (fc_c1_loc(cache_cap_loc)) + ALLOCATE (dfc_c1_loc(cache_cap_loc)) + ALLOCATE (fc_c2_loc(cache_cap_loc)) + ALLOCATE (dfc_c2_loc(cache_cap_loc)) + ALLOCATE (sym_loc(max_ang_symf_loc)) + ALLOCATE (force_loc(3, 3, max_ang_symf_loc)) + +!$OMP DO SCHEDULE(dynamic) + DO s = 1, nnp%ang(ind)%n_symfgrp + cutoff_s = nnp%ang(ind)%symfgrp(s)%cutoff + cutoff_sqr = cutoff_s*cutoff_s + n_symf_s = nnp%ang(ind)%symfgrp(s)%n_symf + n_ang1_s = neighbor%n_ang1(s) + homo_grp = (nnp%ang(ind)%symfgrp(s)%ele(1) == nnp%ang(ind)%symfgrp(s)%ele(2)) + + CALL nnp_fill_fc_dfc_cache(neighbor%ang1(s)%dist, n_ang1_s, & + nnp%cut_type, cutoff_s, fc_c1_loc, dfc_c1_loc) + + IF (homo_grp) THEN + ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & + kk_buf => workspace%dGdr_ang_kk(s)%data) + DO j = 1, n_ang1_s + rvect1 = neighbor%ang1(s)%dist(1:3, j) + r1 = neighbor%ang1(s)%dist(4, j) + DO k = j + 1, n_ang1_s + rvect2 = neighbor%ang1(s)%dist(1:3, k) + r2 = neighbor%ang1(s)%dist(4, k) + rvect3(1) = rvect2(1) - rvect1(1) + rvect3(2) = rvect2(2) - rvect1(2) + rvect3(3) = rvect2(3) - rvect1(3) + r3_sqr = rvect3(1)*rvect3(1) + rvect3(2)*rvect3(2) + rvect3(3)*rvect3(3) + IF (r3_sqr < cutoff_sqr) THEN + r3 = SQRT(r3_sqr) + CALL nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, & + r1, r2, r3, & + fc_c1_loc(j), dfc_c1_loc(j), & + fc_c1_loc(k), dfc_c1_loc(k), & + sym_loc(1:n_symf_s), & + force_loc(:, :, 1:n_symf_s)) + DO sf = 1, n_symf_s + m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) + self_dGdr(1, m) = self_dGdr(1, m) + force_loc(1, 1, sf) + self_dGdr(2, m) = self_dGdr(2, m) + force_loc(2, 1, sf) + self_dGdr(3, m) = self_dGdr(3, m) + force_loc(3, 1, sf) + jj_buf(1, sf, j) = jj_buf(1, sf, j) + force_loc(1, 2, sf) + jj_buf(2, sf, j) = jj_buf(2, sf, j) + force_loc(2, 2, sf) + jj_buf(3, sf, j) = jj_buf(3, sf, j) + force_loc(3, 2, sf) + kk_buf(1, sf, k) = kk_buf(1, sf, k) + force_loc(1, 3, sf) + kk_buf(2, sf, k) = kk_buf(2, sf, k) + force_loc(2, 3, sf) + kk_buf(3, sf, k) = kk_buf(3, sf, k) + force_loc(3, 3, sf) + IF (PRESENT(stress)) THEN + DO l = 1, 3 + stress(:, l, m) = stress(:, l, m) - rvect1(:)*force_loc(l, 2, sf) + stress(:, l, m) = stress(:, l, m) - rvect2(:)*force_loc(l, 3, sf) + END DO + END IF + nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + sym_loc(sf) + END DO + END IF + END DO + END DO + END ASSOCIATE + ELSE + n_ang2_s = neighbor%n_ang2(s) + CALL nnp_fill_fc_dfc_cache(neighbor%ang2(s)%dist, n_ang2_s, & + nnp%cut_type, cutoff_s, fc_c2_loc, dfc_c2_loc) + + ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & + kk_buf => workspace%dGdr_ang_kk(s)%data) + DO j = 1, n_ang1_s + rvect1 = neighbor%ang1(s)%dist(1:3, j) + r1 = neighbor%ang1(s)%dist(4, j) + DO k = 1, n_ang2_s + rvect2 = neighbor%ang2(s)%dist(1:3, k) + r2 = neighbor%ang2(s)%dist(4, k) + rvect3(1) = rvect2(1) - rvect1(1) + rvect3(2) = rvect2(2) - rvect1(2) + rvect3(3) = rvect2(3) - rvect1(3) + r3_sqr = rvect3(1)*rvect3(1) + rvect3(2)*rvect3(2) + rvect3(3)*rvect3(3) + IF (r3_sqr < cutoff_sqr) THEN + r3 = SQRT(r3_sqr) + CALL nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, & + r1, r2, r3, & + fc_c1_loc(j), dfc_c1_loc(j), & + fc_c2_loc(k), dfc_c2_loc(k), & + sym_loc(1:n_symf_s), & + force_loc(:, :, 1:n_symf_s)) + DO sf = 1, n_symf_s + m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) + self_dGdr(1, m) = self_dGdr(1, m) + force_loc(1, 1, sf) + self_dGdr(2, m) = self_dGdr(2, m) + force_loc(2, 1, sf) + self_dGdr(3, m) = self_dGdr(3, m) + force_loc(3, 1, sf) + jj_buf(1, sf, j) = jj_buf(1, sf, j) + force_loc(1, 2, sf) + jj_buf(2, sf, j) = jj_buf(2, sf, j) + force_loc(2, 2, sf) + jj_buf(3, sf, j) = jj_buf(3, sf, j) + force_loc(3, 2, sf) + kk_buf(1, sf, k) = kk_buf(1, sf, k) + force_loc(1, 3, sf) + kk_buf(2, sf, k) = kk_buf(2, sf, k) + force_loc(2, 3, sf) + kk_buf(3, sf, k) = kk_buf(3, sf, k) + force_loc(3, 3, sf) + IF (PRESENT(stress)) THEN + DO l = 1, 3 + stress(:, l, m) = stress(:, l, m) - rvect1(:)*force_loc(l, 2, sf) + stress(:, l, m) = stress(:, l, m) - rvect2(:)*force_loc(l, 3, sf) + END DO + END IF + nnp%ang(ind)%y(m - off) = nnp%ang(ind)%y(m - off) + sym_loc(sf) + END DO + END IF + END DO + END DO + END ASSOCIATE + END IF + END DO +!$OMP END DO + + DEALLOCATE (fc_c1_loc, dfc_c1_loc, fc_c2_loc, dfc_c2_loc, sym_loc, force_loc) +!$OMP END PARALLEL + + END ASSOCIATE + + END SUBROUTINE nnp_acsf_angular_loop_omp + +! ************************************************************************************************** +!> \brief Prepare or update the linked-cell / Verlet cache for the current +!> geometry. Lazily allocates nnp%cell_list_cache and +!> nnp%neighbor_interface_state, then delegates to +!> nnp_prepare_cell_list_cache and nnp_neighbor_interface_prepare. Call +!> once per force eval before the per-atom loop; cheap on re-entry when +!> nothing has changed. +!> \param nnp NNP environment whose persistent caches are to be (re)built. +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_prepare_neighbor_cache(nnp) + + TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp + + IF (.NOT. ALLOCATED(nnp%cell_list_cache)) ALLOCATE (nnp%cell_list_cache) + IF (.NOT. ALLOCATED(nnp%neighbor_interface_state)) ALLOCATE (nnp%neighbor_interface_state) + CALL nnp_prepare_cell_list_cache(nnp) + CALL nnp_neighbor_interface_prepare(nnp) + + END SUBROUTINE nnp_prepare_neighbor_cache + ! ************************************************************************************************** !> \brief Check if the nnp is extrapolating !> \param nnp ... @@ -311,20 +913,16 @@ CONTAINS extrapolate = nnp%output_expol DO j = 1, nnp%n_rad(ind) - IF (nnp%rad(ind)%y(j) - & - nnp%rad(ind)%loc_max(j) > threshold) THEN + IF (nnp%rad(ind)%y(j) - nnp%rad(ind)%loc_max(j) > threshold) THEN extrapolate = .TRUE. - ELSE IF (-nnp%rad(ind)%y(j) + & - nnp%rad(ind)%loc_min(j) > threshold) THEN + ELSE IF (-nnp%rad(ind)%y(j) + nnp%rad(ind)%loc_min(j) > threshold) THEN extrapolate = .TRUE. END IF END DO DO j = 1, nnp%n_ang(ind) - IF (nnp%ang(ind)%y(j) - & - nnp%ang(ind)%loc_max(j) > threshold) THEN + IF (nnp%ang(ind)%y(j) - nnp%ang(ind)%loc_max(j) > threshold) THEN extrapolate = .TRUE. - ELSE IF (-nnp%ang(ind)%y(j) + & - nnp%ang(ind)%loc_min(j) > threshold) THEN + ELSE IF (-nnp%ang(ind)%y(j) + nnp%ang(ind)%loc_min(j) > threshold) THEN extrapolate = .TRUE. END IF END DO @@ -334,76 +932,70 @@ CONTAINS END SUBROUTINE nnp_check_extrapolation ! ************************************************************************************************** -!> \brief Scale and center symetry functions (and gradients) +!> \brief Scale and center symmetry functions (and gradients) !> \param nnp ... !> \param ind ... -!> \param dsymdxyz ... +!> \param do_forces ... !> \param stress ... !> \date 2020-10-10 !> \author Christoph Schran (christoph.schran@rub.de) ! ************************************************************************************************** - SUBROUTINE nnp_scale_acsf(nnp, ind, dsymdxyz, stress) + SUBROUTINE nnp_scale_acsf(nnp, ind, do_forces, stress) TYPE(nnp_type), INTENT(INOUT) :: nnp INTEGER, INTENT(IN) :: ind - REAL(KIND=dp), DIMENSION(:, :, :), INTENT(OUT), & - OPTIONAL :: dsymdxyz, stress + LOGICAL, INTENT(IN) :: do_forces + REAL(KIND=dp), DIMENSION(:, :, :), INTENT(INOUT), & + OPTIONAL :: stress - INTEGER :: j, k, off + INTEGER :: j, k, m, n_ang1_s, n_ang2_s, n_symf_s, & + off, s, sf + LOGICAL :: homo_grp + REAL(KIND=dp) :: scale + +! INOUT (not OUT): stress is per-input-node and accumulates across central atoms. IF (nnp%center_acsf) THEN DO j = 1, nnp%n_rad(ind) - nnp%arc(ind)%layer(1)%node(j) = & - (nnp%rad(ind)%y(j) - nnp%rad(ind)%loc_av(j)) + nnp%arc(ind)%layer(1)%node(j) = nnp%rad(ind)%y(j) - nnp%rad(ind)%loc_av(j) END DO off = nnp%n_rad(ind) DO j = 1, nnp%n_ang(ind) - nnp%arc(ind)%layer(1)%node(j + off) = & - (nnp%ang(ind)%y(j) - nnp%ang(ind)%loc_av(j)) + nnp%arc(ind)%layer(1)%node(j + off) = nnp%ang(ind)%y(j) - nnp%ang(ind)%loc_av(j) END DO IF (nnp%scale_acsf) THEN DO j = 1, nnp%n_rad(ind) - nnp%arc(ind)%layer(1)%node(j) = & - nnp%arc(ind)%layer(1)%node(j)/ & - (nnp%rad(ind)%loc_max(j) - nnp%rad(ind)%loc_min(j))* & - (nnp%scmax - nnp%scmin) + nnp%scmin + nnp%arc(ind)%layer(1)%node(j) = nnp%arc(ind)%layer(1)%node(j)/ & + (nnp%rad(ind)%loc_max(j) - nnp%rad(ind)%loc_min(j))*(nnp%scmax - nnp%scmin) + nnp%scmin END DO off = nnp%n_rad(ind) DO j = 1, nnp%n_ang(ind) - nnp%arc(ind)%layer(1)%node(j + off) = & - nnp%arc(ind)%layer(1)%node(j + off)/ & - (nnp%ang(ind)%loc_max(j) - nnp%ang(ind)%loc_min(j))* & - (nnp%scmax - nnp%scmin) + nnp%scmin + nnp%arc(ind)%layer(1)%node(j + off) = nnp%arc(ind)%layer(1)%node(j + off)/ & + (nnp%ang(ind)%loc_max(j) - nnp%ang(ind)%loc_min(j))*(nnp%scmax - nnp%scmin) + nnp%scmin END DO END IF ELSE IF (nnp%scale_acsf) THEN DO j = 1, nnp%n_rad(ind) - nnp%arc(ind)%layer(1)%node(j) = & - (nnp%rad(ind)%y(j) - nnp%rad(ind)%loc_min(j))/ & - (nnp%rad(ind)%loc_max(j) - nnp%rad(ind)%loc_min(j))* & - (nnp%scmax - nnp%scmin) + nnp%scmin + nnp%arc(ind)%layer(1)%node(j) = (nnp%rad(ind)%y(j) - nnp%rad(ind)%loc_min(j))/ & + (nnp%rad(ind)%loc_max(j) - nnp%rad(ind)%loc_min(j))* & + (nnp%scmax - nnp%scmin) + nnp%scmin END DO off = nnp%n_rad(ind) DO j = 1, nnp%n_ang(ind) - nnp%arc(ind)%layer(1)%node(j + off) = & - (nnp%ang(ind)%y(j) - nnp%ang(ind)%loc_min(j))/ & - (nnp%ang(ind)%loc_max(j) - nnp%ang(ind)%loc_min(j))* & - (nnp%scmax - nnp%scmin) + nnp%scmin + nnp%arc(ind)%layer(1)%node(j + off) = (nnp%ang(ind)%y(j) - nnp%ang(ind)%loc_min(j))/ & + (nnp%ang(ind)%loc_max(j) - nnp%ang(ind)%loc_min(j))* & + (nnp%scmax - nnp%scmin) + nnp%scmin END DO ELSE IF (nnp%scale_sigma_acsf) THEN DO j = 1, nnp%n_rad(ind) - nnp%arc(ind)%layer(1)%node(j) = & - (nnp%rad(ind)%y(j) - nnp%rad(ind)%loc_av(j))/ & - nnp%rad(ind)%sigma(j)* & - (nnp%scmax - nnp%scmin) + nnp%scmin + nnp%arc(ind)%layer(1)%node(j) = (nnp%rad(ind)%y(j) - nnp%rad(ind)%loc_av(j))/ & + nnp%rad(ind)%sigma(j)*(nnp%scmax - nnp%scmin) + nnp%scmin END DO off = nnp%n_rad(ind) DO j = 1, nnp%n_ang(ind) - nnp%arc(ind)%layer(1)%node(j + off) = & - (nnp%ang(ind)%y(j) - nnp%ang(ind)%loc_av(j))/ & - nnp%ang(ind)%sigma(j)* & - (nnp%scmax - nnp%scmin) + nnp%scmin + nnp%arc(ind)%layer(1)%node(j + off) = (nnp%ang(ind)%y(j) - nnp%ang(ind)%loc_av(j))/ & + nnp%ang(ind)%sigma(j)*(nnp%scmax - nnp%scmin) + nnp%scmin END DO ELSE DO j = 1, nnp%n_rad(ind) @@ -415,70 +1007,101 @@ CONTAINS END DO END IF - IF (PRESENT(dsymdxyz)) THEN - IF (nnp%scale_acsf) THEN - DO k = 1, nnp%num_atoms - DO j = 1, nnp%n_rad(ind) - dsymdxyz(:, j, k) = dsymdxyz(:, j, k)/ & - (nnp%rad(ind)%loc_max(j) - & - nnp%rad(ind)%loc_min(j))* & - (nnp%scmax - nnp%scmin) + IF (do_forces .AND. (nnp%scale_acsf .OR. nnp%scale_sigma_acsf)) THEN + ! Scale the per-neighbor dGdr slabs in workspace state. Only the + ! actually-populated entries in each slab need touching: for each + ! group s the valid ranges are sf=1..n_symf_s and j=1..n_rad(s) + ! (radial) or j=1..n_ang1_s, k=1..n_ang2_s (angular). The self + ! contribution lives in self_dGdr(:, m). + ASSOCIATE (workspace => nnp%neighbor_interface_state%workspace(ind), & + neighbor => nnp%neighbor_interface_state%workspace(ind)%neighbor, & + self_dGdr => nnp%neighbor_interface_state%workspace(ind)%self_dGdr) + + ! Radial groups + DO s = 1, nnp%rad(ind)%n_symfgrp + n_symf_s = nnp%rad(ind)%symfgrp(s)%n_symf + ASSOCIATE (rad_buf => workspace%dGdr_rad(s)%data) + DO sf = 1, n_symf_s + m = nnp%rad(ind)%symfgrp(s)%symf(sf) + IF (nnp%scale_acsf) THEN + scale = (nnp%scmax - nnp%scmin)/ & + (nnp%rad(ind)%loc_max(m) - nnp%rad(ind)%loc_min(m)) + ELSE + scale = (nnp%scmax - nnp%scmin)/nnp%rad(ind)%sigma(m) + END IF + self_dGdr(1, m) = self_dGdr(1, m)*scale + self_dGdr(2, m) = self_dGdr(2, m)*scale + self_dGdr(3, m) = self_dGdr(3, m)*scale + DO j = 1, neighbor%n_rad(s) + rad_buf(1, sf, j) = rad_buf(1, sf, j)*scale + rad_buf(2, sf, j) = rad_buf(2, sf, j)*scale + rad_buf(3, sf, j) = rad_buf(3, sf, j)*scale + END DO END DO + END ASSOCIATE END DO + + ! Angular groups off = nnp%n_rad(ind) - DO k = 1, nnp%num_atoms - DO j = 1, nnp%n_ang(ind) - dsymdxyz(:, j + off, k) = dsymdxyz(:, j + off, k)/ & - (nnp%ang(ind)%loc_max(j) - & - nnp%ang(ind)%loc_min(j))* & - (nnp%scmax - nnp%scmin) + DO s = 1, nnp%ang(ind)%n_symfgrp + n_symf_s = nnp%ang(ind)%symfgrp(s)%n_symf + n_ang1_s = neighbor%n_ang1(s) + homo_grp = (nnp%ang(ind)%symfgrp(s)%ele(1) == nnp%ang(ind)%symfgrp(s)%ele(2)) + IF (homo_grp) THEN + ! kk slab is also indexed in ind_ang1 in the homo case + n_ang2_s = n_ang1_s + ELSE + n_ang2_s = neighbor%n_ang2(s) + END IF + ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & + kk_buf => workspace%dGdr_ang_kk(s)%data) + DO sf = 1, n_symf_s + m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) + IF (nnp%scale_acsf) THEN + scale = (nnp%scmax - nnp%scmin)/ & + (nnp%ang(ind)%loc_max(m - off) - nnp%ang(ind)%loc_min(m - off)) + ELSE + scale = (nnp%scmax - nnp%scmin)/nnp%ang(ind)%sigma(m - off) + END IF + self_dGdr(1, m) = self_dGdr(1, m)*scale + self_dGdr(2, m) = self_dGdr(2, m)*scale + self_dGdr(3, m) = self_dGdr(3, m)*scale + DO j = 1, n_ang1_s + jj_buf(1, sf, j) = jj_buf(1, sf, j)*scale + jj_buf(2, sf, j) = jj_buf(2, sf, j)*scale + jj_buf(3, sf, j) = jj_buf(3, sf, j)*scale + END DO + DO k = 1, n_ang2_s + kk_buf(1, sf, k) = kk_buf(1, sf, k)*scale + kk_buf(2, sf, k) = kk_buf(2, sf, k)*scale + kk_buf(3, sf, k) = kk_buf(3, sf, k)*scale + END DO END DO + END ASSOCIATE END DO - ELSE IF (nnp%scale_sigma_acsf) THEN - DO k = 1, nnp%num_atoms - DO j = 1, nnp%n_rad(ind) - dsymdxyz(:, j, k) = dsymdxyz(:, j, k)/ & - nnp%rad(ind)%sigma(j)* & - (nnp%scmax - nnp%scmin) - END DO - END DO - off = nnp%n_rad(ind) - DO k = 1, nnp%num_atoms - DO j = 1, nnp%n_ang(ind) - dsymdxyz(:, j + off, k) = dsymdxyz(:, j + off, k)/ & - nnp%ang(ind)%sigma(j)* & - (nnp%scmax - nnp%scmin) - END DO - END DO - END IF + + END ASSOCIATE END IF IF (PRESENT(stress)) THEN IF (nnp%scale_acsf) THEN DO j = 1, nnp%n_rad(ind) - stress(:, :, j) = stress(:, :, j)/ & - (nnp%rad(ind)%loc_max(j) - & - nnp%rad(ind)%loc_min(j))* & + stress(:, :, j) = stress(:, :, j)/(nnp%rad(ind)%loc_max(j) - nnp%rad(ind)%loc_min(j))* & (nnp%scmax - nnp%scmin) END DO off = nnp%n_rad(ind) DO j = 1, nnp%n_ang(ind) stress(:, :, j + off) = stress(:, :, j + off)/ & - (nnp%ang(ind)%loc_max(j) - & - nnp%ang(ind)%loc_min(j))* & + (nnp%ang(ind)%loc_max(j) - nnp%ang(ind)%loc_min(j))* & (nnp%scmax - nnp%scmin) END DO ELSE IF (nnp%scale_sigma_acsf) THEN DO j = 1, nnp%n_rad(ind) - stress(:, :, j) = stress(:, :, j)/ & - nnp%rad(ind)%sigma(j)* & - (nnp%scmax - nnp%scmin) + stress(:, :, j) = stress(:, :, j)/nnp%rad(ind)%sigma(j)*(nnp%scmax - nnp%scmin) END DO off = nnp%n_rad(ind) DO j = 1, nnp%n_ang(ind) - stress(:, :, j + off) = stress(:, :, j + off)/ & - nnp%ang(ind)%sigma(j)* & - (nnp%scmax - nnp%scmin) + stress(:, :, j + off) = stress(:, :, j + off)/nnp%ang(ind)%sigma(j)*(nnp%scmax - nnp%scmin) END DO END IF END IF @@ -486,8 +1109,7 @@ CONTAINS END SUBROUTINE nnp_scale_acsf ! ************************************************************************************************** -!> \brief Calculate radial symmetry function and gradient (optinal) -!> for given displacment vecotr rvect of atom i and j +!> \brief Calculate radial symmetry function and gradient (optional) !> \param nnp ... !> \param ind ... !> \param s ... @@ -500,7 +1122,7 @@ CONTAINS ! ************************************************************************************************** SUBROUTINE nnp_calc_rad(nnp, ind, s, rvect, r, sym, force) - TYPE(nnp_type), INTENT(IN) :: nnp + TYPE(nnp_type), INTENT(IN), TARGET :: nnp INTEGER, INTENT(IN) :: ind, s REAL(KIND=dp), DIMENSION(3), INTENT(IN) :: rvect REAL(KIND=dp), INTENT(IN) :: r @@ -508,62 +1130,197 @@ CONTAINS REAL(KIND=dp), DIMENSION(:, :), INTENT(OUT), & OPTIONAL :: force - INTEGER :: k, sf - REAL(KIND=dp) :: dfcutdr, dsymdr, eta, fcut, funccut, rs, & - tmp - REAL(KIND=dp), DIMENSION(3) :: drdx + INTEGER :: i, n_symf, sf + REAL(KIND=dp) :: dh00, dh01, dh10, dh11, drdx_x, drdx_y, & + drdx_z, dsymdr_full, dyi, dyi1, h00, & + h01, h10, h10_dx, h11, h11_dx, r_inv, & + t, t2, t3, yi, yi1 + TYPE(nnp_symfgrp_type), POINTER :: grp - !init - drdx = 0.0_dp - fcut = 0.0_dp - dfcutdr = 0.0_dp + grp => nnp%rad(ind)%symfgrp(s) + n_symf = grp%n_symf - !Calculate cutoff function and partial derivative - funccut = nnp%rad(ind)%symfgrp(s)%cutoff !cutoff - SELECT CASE (nnp%cut_type) - CASE (nnp_cut_cos) - tmp = pi*r/funccut - fcut = 0.5_dp*(COS(tmp) + 1.0_dp) + ! Group-shared Hermite cubic spline. All SFs in this radial group share + ! grp%cutoff, hence the same uniform grid, so the interpolation parameters + ! (i, t, h00..h11, dh00..dh11) are computed once outside the SF loop; the + ! inner loop then streams 4 contiguous reads per SF from spline_y/spline_dy. + ! + ! Out-of-range clamping: for r past spline_x_max (= grp%cutoff) every SF + ! sees the boundary value of its tabulated y(r), which the build routine + ! pinned to 0 since fcut(cutoff) = 0, so the clamp returns sym = 0. + IF (r >= grp%spline_x_max) THEN + DO sf = 1, n_symf + sym(sf) = 0.0_dp + END DO IF (PRESENT(force)) THEN - dfcutdr = 0.5_dp*(-SIN(tmp))*(pi/funccut) + DO sf = 1, n_symf + force(1, sf) = 0.0_dp + force(2, sf) = 0.0_dp + force(3, sf) = 0.0_dp + END DO END IF - CASE (nnp_cut_tanh) - tmp = TANH(1.0_dp - r/funccut) - fcut = tmp**3 - IF (PRESENT(force)) THEN - dfcutdr = (-3.0_dp/funccut)*(tmp**2 - tmp**4) - END IF - CASE DEFAULT - CPABORT("NNP| Cutoff function unknown") - END SELECT + RETURN + END IF - IF (PRESENT(force)) drdx(:) = rvect(:)/r + i = INT(r*grp%spline_dx_inv) + 1 + IF (i < 1) i = 1 + IF (i > grp%spline_n - 1) i = grp%spline_n - 1 - !loop over sym fncts of sym fnct group s - DO sf = 1, nnp%rad(ind)%symfgrp(s)%n_symf - k = nnp%rad(ind)%symfgrp(s)%symf(sf) !symf indice - eta = nnp%rad(ind)%eta(k) !eta - rs = nnp%rad(ind)%rs(k) !rshift + t = (r - REAL(i - 1, KIND=dp)*grp%spline_dx)*grp%spline_dx_inv + t2 = t*t + t3 = t2*t - ! Calculate radial symmetry function - sym(sf) = EXP(-eta*(r - rs)**2) + h00 = 2.0_dp*t3 - 3.0_dp*t2 + 1.0_dp + h10 = t3 - 2.0_dp*t2 + t + h01 = -2.0_dp*t3 + 3.0_dp*t2 + h11 = t3 - t2 + h10_dx = h10*grp%spline_dx + h11_dx = h11*grp%spline_dx - ! Calculate partial derivatives of symmetry function and distance - ! and combine them to obtain force - IF (PRESENT(force)) THEN - dsymdr = sym(sf)*(-2.0_dp*eta*(r - rs)) - force(:, sf) = fcut*dsymdr*drdx(:) + sym(sf)*dfcutdr*drdx(:) - END IF + IF (PRESENT(force)) THEN + dh00 = 6.0_dp*(t2 - t) + dh10 = 3.0_dp*t2 - 4.0_dp*t + 1.0_dp + dh01 = -dh00 + dh11 = 3.0_dp*t2 - 2.0_dp*t - ! combine radial symmetry function and cutoff function - sym(sf) = sym(sf)*fcut - END DO + r_inv = 1.0_dp/r + drdx_x = rvect(1)*r_inv + drdx_y = rvect(2)*r_inv + drdx_z = rvect(3)*r_inv + + ! Vectorizable inner loop: contiguous reads on sf, no branches, + ! no function calls, no transcendentals. + ASSOCIATE (spy_i => grp%spline_y(:, i), spy_i1 => grp%spline_y(:, i + 1), & + spdy_i => grp%spline_dy(:, i), spdy_i1 => grp%spline_dy(:, i + 1)) + !$OMP SIMD PRIVATE(yi, yi1, dyi, dyi1, dsymdr_full) + DO sf = 1, n_symf + yi = spy_i(sf) + yi1 = spy_i1(sf) + dyi = spdy_i(sf) + dyi1 = spdy_i1(sf) + sym(sf) = h00*yi + h10_dx*dyi + h01*yi1 + h11_dx*dyi1 + dsymdr_full = (dh00*yi + dh01*yi1)*grp%spline_dx_inv + dh10*dyi + dh11*dyi1 + force(1, sf) = dsymdr_full*drdx_x + force(2, sf) = dsymdr_full*drdx_y + force(3, sf) = dsymdr_full*drdx_z + END DO + END ASSOCIATE + ELSE + ASSOCIATE (spy_i => grp%spline_y(:, i), spy_i1 => grp%spline_y(:, i + 1), & + spdy_i => grp%spline_dy(:, i), spdy_i1 => grp%spline_dy(:, i + 1)) + !$OMP SIMD + DO sf = 1, n_symf + sym(sf) = h00*spy_i(sf) + h10_dx*spdy_i(sf) + & + h01*spy_i1(sf) + h11_dx*spdy_i1(sf) + END DO + END ASSOCIATE + END IF END SUBROUTINE nnp_calc_rad ! ************************************************************************************************** -!> \brief Calculate angular symmetry function and gradient (optinal) -!> for given displacment vectors rvect1,2,3 of atom i,j and k +!> \brief Build per-radial-group Hermite cubic spline tables. +!> Each radial group s carries its own dense (n_symf, n_grid) tables +!> spline_y and spline_dy. Within a group all SFs share the cutoff +!> => same uniform grid, so the inner SF loop in nnp_calc_rad can +!> compute the Hermite basis params ONCE and stream the per-SF y/dy +!> values contiguously (sf-first layout). +!> +!> Each table stores y(r) = EXP(-eta_sf*(r-rs_sf)^2)*fcut(r) and its +!> analytic derivative y'(r). Memory cost: 2*n_symf*n_grid doubles +!> per group, ~128 KB for n_symf=8, n_grid=8192. Built once per +!> force eval; lives until nnp_env_release. +!> \param nnp ... +! ************************************************************************************************** + SUBROUTINE nnp_build_radial_splines(nnp) + + TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp + + INTEGER :: ind, k, n_symf, p, s, sf + REAL(KIND=dp) :: arg, cutoff, dfcutdr, dr, eta, exp_term, & + fcut, r, rs, tanh_tmp + + DO ind = 1, nnp%n_ele + DO s = 1, nnp%rad(ind)%n_symfgrp + ASSOCIATE (grp => nnp%rad(ind)%symfgrp(s)) + n_symf = grp%n_symf + cutoff = grp%cutoff + dr = cutoff/REAL(nnp%rad_spline_n - 1, KIND=dp) + + IF (ALLOCATED(grp%spline_y)) DEALLOCATE (grp%spline_y) + IF (ALLOCATED(grp%spline_dy)) DEALLOCATE (grp%spline_dy) + ALLOCATE (grp%spline_y(MAX(1, n_symf), nnp%rad_spline_n)) + ALLOCATE (grp%spline_dy(MAX(1, n_symf), nnp%rad_spline_n)) + grp%spline_n = nnp%rad_spline_n + grp%spline_dx = dr + grp%spline_dx_inv = 1.0_dp/dr + grp%spline_x_max = cutoff + + ! Fill the table SF-by-SF. The grid loop is the inner one + ! during build only; runtime nnp_calc_rad reads sf-first. + DO sf = 1, n_symf + k = grp%symf(sf) + eta = nnp%rad(ind)%eta(k) + rs = nnp%rad(ind)%rs(k) + + DO p = 1, nnp%rad_spline_n + r = REAL(p - 1, KIND=dp)*dr + + SELECT CASE (nnp%cut_type) + CASE (nnp_cut_cos) + arg = pi*r/cutoff + fcut = 0.5_dp*(COS(arg) + 1.0_dp) + dfcutdr = -0.5_dp*SIN(arg)*(pi/cutoff) + CASE (nnp_cut_tanh) + tanh_tmp = TANH(1.0_dp - r/cutoff) + fcut = tanh_tmp**3 + dfcutdr = (-3.0_dp/cutoff)*(tanh_tmp**2 - tanh_tmp**4) + CASE DEFAULT + CPABORT("NNP| Cutoff function unknown") + END SELECT + + exp_term = EXP(-eta*(r - rs)**2) + + grp%spline_y(sf, p) = exp_term*fcut + grp%spline_dy(sf, p) = exp_term*(-2.0_dp*eta*(r - rs))*fcut + & + exp_term*dfcutdr + END DO + END DO + + ! Pin the boundary so the runtime out-of-range branch can + ! return zeros without re-checking each SF. + DO sf = 1, n_symf + grp%spline_y(sf, nnp%rad_spline_n) = 0.0_dp + grp%spline_dy(sf, nnp%rad_spline_n) = 0.0_dp + END DO + + grp%spline_built = .TRUE. + END ASSOCIATE + END DO + END DO + + END SUBROUTINE nnp_build_radial_splines + +! ************************************************************************************************** +!> \brief Calculate angular symmetry function and gradient (optional) +!> +!> Vectorized SF-batched form. The original eta-sorted exp-skip loop has +!> been replaced by a sequence of SF passes: +!> 1. Branchless angular base: clamps tmp at the cusp (tmp <= 0), then +!> computes tmpzeta via integer or real pow per SF. Stays scalar +!> because the int/real switch and integer pow cannot SIMD. +!> 2. Vectorized EXP: symtmp_arr(sf) = EXP(-eta(sf)*r2sum). Marked +!> !$OMP SIMD so the compiler maps it to libmvec/SVML vector EXP. +!> Vector libm hides EXP latency across the SF loop without needing +!> an eta-dedup pre-pass. +!> 3. SIMD sym scatter: sym(sf) = prefzeta*angular*symtmp*ftot. +!> 4. Force scatter (only when forces requested). The clamp in pass 1 +!> forces tmpzeta_arr(sf) = 0 at the cusp, which makes both angular +!> AND pref_lam vanish naturally -- no per-iter branch. +!> +!> Caller passes precomputed cutoff values for the j/k legs; the j-k leg +!> is computed inline. Geometry-only derivative bases are hoisted outside +!> the SF loops as before. !> \param nnp ... !> \param ind ... !> \param s ... @@ -573,183 +1330,182 @@ CONTAINS !> \param r1 ... !> \param r2 ... !> \param r3 ... +!> \param fcut_j ... +!> \param dfcut_j ... +!> \param fcut_k ... +!> \param dfcut_k ... !> \param sym ... !> \param force ... !> \date 2020-10-10 !> \author Christoph Schran (christoph.schran@rub.de) ! ************************************************************************************************** - SUBROUTINE nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, r1, r2, r3, sym, force) + SUBROUTINE nnp_calc_ang(nnp, ind, s, rvect1, rvect2, rvect3, r1, r2, r3, & + fcut_j, dfcut_j, fcut_k, dfcut_k, sym, force) - TYPE(nnp_type), INTENT(IN) :: nnp + TYPE(nnp_type), INTENT(IN), TARGET :: nnp INTEGER, INTENT(IN) :: ind, s REAL(KIND=dp), DIMENSION(3), INTENT(IN) :: rvect1, rvect2, rvect3 - REAL(KIND=dp), INTENT(IN) :: r1, r2, r3 + REAL(KIND=dp), INTENT(IN) :: r1, r2, r3, fcut_j, dfcut_j, fcut_k, & + dfcut_k REAL(KIND=dp), DIMENSION(:), INTENT(OUT) :: sym REAL(KIND=dp), DIMENSION(:, :, :), INTENT(OUT), & OPTIONAL :: force - INTEGER :: i, m, sf - REAL(KIND=dp) :: angular, costheta, dfcutdr1, dfcutdr2, dfcutdr3, dsymdr1, dsymdr2, dsymdr3, & - eta, f, fcut, fcut1, fcut2, fcut3, ftot, g, lam, pref, prefzeta, rsqr1, rsqr2, rsqr3, & - symtmp, tmp, tmp1, tmp2, tmp3, tmpzeta, zeta - REAL(KIND=dp), DIMENSION(3) :: dangulardx1, dangulardx2, dangulardx3, dcosthetadx1, & - dcosthetadx2, dcosthetadx3, dfdx1, dfdx2, dfdx3, dgdx1, dgdx2, dgdx3, dr1dx, dr2dx, dr3dx + INTEGER :: ii, izeta, n_symf, sf + LOGICAL :: do_forces + REAL(KIND=dp) :: angular, arg_tmp, costheta, dfcut3, dfcutdr1, dfcutdr2, dfcutdr3, dsymdr1, & + dsymdr2, dsymdr3, eta, f, fcut3, fcut_rc, ftot, g, inv_g2, lam, pref_lam, prefzeta, & + r2sum, rsqr1, rsqr2, rsqr3, symtmp, tanh_tmp, tmp, tmp1, tmp2, tmp3, tmpzeta, zeta + REAL(KIND=dp), DIMENSION(3) :: dcosbase1, dcosbase2, dcosbase3, dgdx1, & + dgdx2, dgdx3, dr1dx, dr2dx, dr3dx + TYPE(nnp_symfgrp_type), POINTER :: grp + REAL(KIND=dp), & + DIMENSION(nnp%ang(ind)%symfgrp(s)%n_symf) :: angular_arr, symtmp_arr, tmpzeta_arr - rsqr1 = r1**2 - rsqr2 = r2**2 - rsqr3 = r3**2 +! Per-SF scratch (automatic arrays sized to the current group). For +! typical n2p2 ACSF n_symf is 8-32, so this is ~256-1024 bytes on +! stack per call. Holds the staged outputs of pass 1 + pass 2 so +! pass 3 streams them contiguously. - !init - dangulardx1 = 0.0_dp - dangulardx2 = 0.0_dp - dangulardx3 = 0.0_dp - dr1dx = 0.0_dp - dr2dx = 0.0_dp - dr3dx = 0.0_dp - ftot = 0.0_dp - dfcutdr1 = 0.0_dp - dfcutdr2 = 0.0_dp - dfcutdr3 = 0.0_dp + do_forces = PRESENT(force) + grp => nnp%ang(ind)%symfgrp(s) + n_symf = grp%n_symf + fcut_rc = grp%cutoff - ! Calculate cos(theta) - ! use law of cosine for theta - ! cos(ang(r1,r2)) = (r3**2 - r1**2 - r2**2)/(-2*r1*r2) - ! | f | g | - f = (rsqr3 - rsqr1 - rsqr2) + rsqr1 = r1*r1 + rsqr2 = r2*r2 + rsqr3 = r3*r3 + r2sum = rsqr1 + rsqr2 + rsqr3 + + f = rsqr3 - rsqr1 - rsqr2 g = -2.0_dp*r1*r2 costheta = f/g - ! Calculate cutoff function and partial derivatives - fcut = nnp%ang(ind)%symfgrp(s)%cutoff !cutoff + ! Compute fcut3 for r3 (j-k distance -- cannot be precomputed by caller) SELECT CASE (nnp%cut_type) CASE (nnp_cut_cos) - tmp1 = pi*r1/fcut - tmp2 = pi*r2/fcut - tmp3 = pi*r3/fcut - fcut1 = 0.5_dp*(COS(tmp1) + 1.0_dp) - fcut2 = 0.5_dp*(COS(tmp2) + 1.0_dp) - fcut3 = 0.5_dp*(COS(tmp3) + 1.0_dp) - ftot = fcut1*fcut2*fcut3 - IF (PRESENT(force)) THEN - pref = 0.5_dp*(pi/fcut) - dfcutdr1 = pref*(-SIN(tmp1))*fcut2*fcut3 - dfcutdr2 = pref*(-SIN(tmp2))*fcut1*fcut3 - dfcutdr3 = pref*(-SIN(tmp3))*fcut1*fcut2 - END IF + arg_tmp = pi*r3/fcut_rc + fcut3 = 0.5_dp*(COS(arg_tmp) + 1.0_dp) + IF (do_forces) dfcut3 = -0.5_dp*SIN(arg_tmp)*(pi/fcut_rc) CASE (nnp_cut_tanh) - tmp1 = TANH(1.0_dp - r1/fcut) - tmp2 = TANH(1.0_dp - r2/fcut) - tmp3 = TANH(1.0_dp - r3/fcut) - fcut1 = tmp1**3 - fcut2 = tmp2**3 - fcut3 = tmp3**3 - ftot = fcut1*fcut2*fcut3 - IF (PRESENT(force)) THEN - pref = -3.0_dp/fcut - dfcutdr1 = pref*(tmp1**2 - tmp1**4)*fcut2*fcut3 - dfcutdr2 = pref*(tmp2**2 - tmp2**4)*fcut1*fcut3 - dfcutdr3 = pref*(tmp3**2 - tmp3**4)*fcut1*fcut2 - END IF + tanh_tmp = TANH(1.0_dp - r3/fcut_rc) + fcut3 = tanh_tmp**3 + IF (do_forces) dfcut3 = (-3.0_dp/fcut_rc)*(tanh_tmp**2 - tanh_tmp**4) CASE DEFAULT CPABORT("NNP| Cutoff function unknown") END SELECT - IF (PRESENT(force)) THEN + ! Use precomputed fcut values for j and k neighbors + ftot = fcut_j*fcut_k*fcut3 + + IF (do_forces) THEN + ! Combined cutoff derivatives (product rule) + dfcutdr1 = dfcut_j*fcut_k*fcut3 + dfcutdr2 = fcut_j*dfcut_k*fcut3 + dfcutdr3 = fcut_j*fcut_k*dfcut3 + dr1dx(:) = rvect1(:)/r1 dr2dx(:) = rvect2(:)/r2 dr3dx(:) = rvect3(:)/r3 + + ! Hoist geometry-only parts of costheta derivatives outside SF loop. + ! Full angular derivative: dangulardx = (zeta*tmpzeta*lam/g^2) * dcosbase + ! where dcosbase factors out lam from dfdx and the f*dgdx term. + ! inv_g2 = 1/g^2 is hoisted: one divide per triplet, one mul per SF. + inv_g2 = 1.0_dp/(g*g) + DO ii = 1, 3 + tmp1 = 2.0_dp*r2*dr1dx(ii) + tmp2 = 2.0_dp*r1*dr2dx(ii) + dgdx1(ii) = -(tmp1 + tmp2) + dgdx2(ii) = tmp1 + dgdx3(ii) = tmp2 + + dcosbase1(ii) = -2.0_dp*(rvect1(ii) + rvect2(ii))*g - f*dgdx1(ii) + dcosbase2(ii) = 2.0_dp*(rvect3(ii) + rvect1(ii))*g - f*dgdx2(ii) + dcosbase3(ii) = 2.0_dp*(rvect2(ii) - rvect3(ii))*g - f*dgdx3(ii) + END DO + ELSE + inv_g2 = 0.0_dp END IF - !loop over associated sym fncts - DO sf = 1, nnp%ang(ind)%symfgrp(s)%n_symf - m = nnp%ang(ind)%symfgrp(s)%symf(sf) - lam = nnp%ang(ind)%lam(m) !lambda - zeta = nnp%ang(ind)%zeta(m) !zeta - prefzeta = nnp%ang(ind)%prefzeta(m) ! 2**(1-zeta) - eta = nnp%ang(ind)%eta(m) !eta - - tmp = (1.0_dp + lam*costheta) - + ! ---- Pass 1: angular base + tmpzeta (scalar tight loop) ---- + ! Branchless cusp handling: when tmp <= 0 we set tmpzeta_arr = 0, which + ! propagates to both angular_arr and pref_lam in pass 3, giving + ! sym = 0 and force = 0 without an explicit CYCLE branch. + DO sf = 1, n_symf + tmp = 1.0_dp + grp%pack_lam(sf)*costheta IF (tmp <= 0.0_dp) THEN - sym(sf) = 0.0_dp - IF (PRESENT(force)) force(:, :, sf) = 0.0_dp - CYCLE - END IF - - ! Calculate symmetry function - ! Calculate angular symmetry function - ! ang = (1+lam*cos(theta_ijk))**zeta - i = NINT(zeta) - IF (1.0_dp*i == zeta) THEN - tmpzeta = tmp**(i - 1) ! integer power is a LOT faster + tmpzeta_arr(sf) = 0.0_dp + angular_arr(sf) = 0.0_dp ELSE - tmpzeta = tmp**(zeta - 1.0_dp) + IF (grp%pack_use_int_zeta(sf)) THEN + izeta = grp%pack_izeta(sf) + tmpzeta_arr(sf) = tmp**(izeta - 1) + ELSE + tmpzeta_arr(sf) = tmp**(grp%pack_zeta(sf) - 1.0_dp) + END IF + angular_arr(sf) = tmpzeta_arr(sf)*tmp END IF - angular = tmpzeta*tmp - ! exponential part: - ! exp(-eta*(r1**2+r2**2+r3**2)) - symtmp = EXP(-eta*(rsqr1 + rsqr2 + rsqr3)) + END DO - ! Partial derivatives (f/g)' = (f'g - fg')/g^2 - IF (PRESENT(force)) THEN - pref = zeta*(tmpzeta)/g**2 - DO i = 1, 3 - dfdx1(i) = -2.0_dp*lam*(rvect1(i) + rvect2(i)) - dfdx2(i) = 2.0_dp*lam*(rvect3(i) + rvect1(i)) - dfdx3(i) = 2.0_dp*lam*(rvect2(i) - rvect3(i)) + ! ---- Pass 2: vectorized EXP via libmvec/SVML ---- + ! Single SIMD loop computing symtmp_arr(sf) = EXP(-eta(sf)*r2sum). + ! With -fopenmp-simd this maps to vector libm (e.g. _ZGVdN4v_exp on + ! AVX2, _ZGVeN8v_exp on AVX-512), hiding EXP latency across 4-8 SFs + ! per iter -- ~10 cycles/element vs ~30 for scalar EXP, which beats + ! any eta-dedup pre-pass. + !$OMP SIMD + DO sf = 1, n_symf + symtmp_arr(sf) = EXP(-grp%pack_eta(sf)*r2sum) + END DO - tmp1 = 2.0_dp*r2*dr1dx(i) - tmp2 = 2.0_dp*r1*dr2dx(i) - dgdx1(i) = -(tmp1 + tmp2) - dgdx2(i) = tmp1 - dgdx3(i) = tmp2 + ! ---- Pass 3: sym scatter (vectorizable) ---- + !$OMP SIMD + DO sf = 1, n_symf + sym(sf) = grp%pack_prefzeta(sf)*angular_arr(sf)*symtmp_arr(sf)*ftot + END DO - dcosthetadx1(i) = dfdx1(i)*g - lam*f*dgdx1(i) - dcosthetadx2(i) = dfdx2(i)*g - lam*f*dgdx2(i) - dcosthetadx3(i) = dfdx3(i)*g - lam*f*dgdx3(i) + ! ---- Pass 4: force scatter (only when do_forces) ---- + IF (do_forces) THEN + DO sf = 1, n_symf + symtmp = symtmp_arr(sf) + angular = angular_arr(sf) + tmpzeta = tmpzeta_arr(sf) + eta = grp%pack_eta(sf) + lam = grp%pack_lam(sf) + zeta = grp%pack_zeta(sf) + prefzeta = grp%pack_prefzeta(sf) - dangulardx1(i) = pref*dcosthetadx1(i) - dangulardx2(i) = pref*dcosthetadx2(i) - dangulardx3(i) = pref*dcosthetadx3(i) - END DO + ! pref_lam carries tmpzeta, so it vanishes at the cusp without + ! an explicit branch (tmpzeta_arr was clamped to 0 in pass 1). + pref_lam = zeta*tmpzeta*lam*inv_g2 - ! Calculate partial derivatives of exponential part and distance - ! and combine partial derivatives to obtain force - pref = prefzeta tmp = -2.0_dp*symtmp*eta dsymdr1 = tmp*r1 dsymdr2 = tmp*r2 dsymdr3 = tmp*r3 - ! G(r1,r2,r3) = pref*angular(r1,r2,r3)*exp(r1,r2,r3)*fcut(r1,r2,r3) - ! dG/dx1 = (dangular/dx1* exp * fcut + - ! angular * dexp/dx1* fcut + - ! angular * exp * dfcut/dx1)*pref - ! dr1/dx1 = -dr1/dx2 - tmp = pref*symtmp*ftot - tmp1 = pref*angular*(ftot*dsymdr1 + dfcutdr1*symtmp) - tmp2 = pref*angular*(ftot*dsymdr2 + dfcutdr2*symtmp) - tmp3 = pref*angular*(ftot*dsymdr3 + dfcutdr3*symtmp) - DO i = 1, 3 - force(i, 1, sf) = tmp*dangulardx1(i) + tmp1*dr1dx(i) + tmp2*dr2dx(i) - force(i, 2, sf) = tmp*dangulardx2(i) - tmp1*dr1dx(i) + tmp3*dr3dx(i) - force(i, 3, sf) = tmp*dangulardx3(i) - tmp2*dr2dx(i) - tmp3*dr3dx(i) + tmp = prefzeta*symtmp*ftot + tmp1 = prefzeta*angular*(ftot*dsymdr1 + dfcutdr1*symtmp) + tmp2 = prefzeta*angular*(ftot*dsymdr2 + dfcutdr2*symtmp) + tmp3 = prefzeta*angular*(ftot*dsymdr3 + dfcutdr3*symtmp) + DO ii = 1, 3 + force(ii, 1, sf) = tmp*pref_lam*dcosbase1(ii) + tmp1*dr1dx(ii) + tmp2*dr2dx(ii) + force(ii, 2, sf) = tmp*pref_lam*dcosbase2(ii) - tmp1*dr1dx(ii) + tmp3*dr3dx(ii) + force(ii, 3, sf) = tmp*pref_lam*dcosbase3(ii) - tmp2*dr2dx(ii) - tmp3*dr3dx(ii) END DO - END IF - - ! Don't forget prefactor: 2**(1-ang%zeta) - pref = prefzeta - ! combine angular and exponential part with cutoff function - sym(sf) = pref*angular*symtmp*ftot - END DO + END DO + END IF END SUBROUTINE nnp_calc_ang ! ************************************************************************************************** -!> \brief Sort element array according to atomic number -!> \param ele ... -!> \param nuc_ele ... -!> \date 2020-10-10 +!> \brief Sort an (ele, nuc_ele) pair of arrays in ascending order of atomic number. +!> Used to canonicalise element ordering inside the NNP environment so the +!> same model file produces the same per-element index layout regardless +!> of input ordering. +!> \param ele element-symbol array, sorted in place to match nuc_ele. +!> \param nuc_ele per-element atomic number, sorted in place. !> \author Christoph Schran (christoph.schran@rub.de) ! ************************************************************************************************** SUBROUTINE nnp_sort_ele(ele, nuc_ele) @@ -759,12 +1515,10 @@ CONTAINS CHARACTER(len=2) :: tmp_ele INTEGER :: i, j, loc, minimum, tmp_nuc_ele - ! Determine atomic number DO i = 1, SIZE(ele) CALL get_ptable_info(ele(i), number=nuc_ele(i)) END DO - ! Sort both arrays DO i = 1, SIZE(ele) - 1 minimum = nuc_ele(i) loc = i @@ -781,15 +1535,16 @@ CONTAINS tmp_ele = ele(i) ele(i) = ele(loc) ele(loc) = tmp_ele - END DO END SUBROUTINE nnp_sort_ele ! ************************************************************************************************** -!> \brief Sort symmetry functions according to different criteria -!> \param nnp ... -!> \date 2020-10-10 +!> \brief Sort radial and angular symmetry functions in canonical order. +!> Radial SFs are sorted by eta (ascending) then rcut; angular SFs by +!> eta, lambda, zeta. This is the order downstream code in nnp_init_acsf_groups +!> relies on for run-length-style group packing. +!> \param nnp NNP environment whose rad/ang SF arrays will be reordered in place. !> \author Christoph Schran (christoph.schran@rub.de) ! ************************************************************************************************** SUBROUTINE nnp_sort_acsf(nnp) @@ -797,12 +1552,7 @@ CONTAINS INTEGER :: i, j, k, loc - ! First sort is according to symmetry function type - ! This is done manually, since data structures are separate - ! Note: Bubble sort is OK here, since small sort + special DO i = 1, nnp%n_ele - ! sort by cutoff - ! rad DO j = 1, nnp%n_rad(i) - 1 loc = j DO k = j + 1, nnp%n_rad(i) @@ -810,12 +1560,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swaprad(nnp%rad(i), j, loc) END DO - ! sort by eta - ! rad DO j = 1, nnp%n_rad(i) - 1 loc = j DO k = j + 1, nnp%n_rad(i) @@ -824,12 +1571,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swaprad(nnp%rad(i), j, loc) END DO - ! sort by rshift - ! rad DO j = 1, nnp%n_rad(i) - 1 loc = j DO k = j + 1, nnp%n_rad(i) @@ -839,12 +1583,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swaprad(nnp%rad(i), j, loc) END DO - ! sort by ele - ! rad DO j = 1, nnp%n_rad(i) - 1 loc = j DO k = j + 1, nnp%n_rad(i) @@ -855,12 +1596,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swaprad(nnp%rad(i), j, loc) END DO - ! ang - ! sort by cutoff DO j = 1, nnp%n_ang(i) - 1 loc = j DO k = j + 1, nnp%n_ang(i) @@ -868,12 +1606,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swapang(nnp%ang(i), j, loc) END DO - ! sort by eta - ! ang DO j = 1, nnp%n_ang(i) - 1 loc = j DO k = j + 1, nnp%n_ang(i) @@ -882,12 +1617,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swapang(nnp%ang(i), j, loc) END DO - ! sort by zeta - ! ang DO j = 1, nnp%n_ang(i) - 1 loc = j DO k = j + 1, nnp%n_ang(i) @@ -897,12 +1629,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swapang(nnp%ang(i), j, loc) END DO - ! sort by lambda - ! ang DO j = 1, nnp%n_ang(i) - 1 loc = j DO k = j + 1, nnp%n_ang(i) @@ -913,12 +1642,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swapang(nnp%ang(i), j, loc) END DO - ! sort by ele - ! ang, ele1 DO j = 1, nnp%n_ang(i) - 1 loc = j DO k = j + 1, nnp%n_ang(i) @@ -930,10 +1656,9 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swapang(nnp%ang(i), j, loc) END DO - ! ang, ele2 + DO j = 1, nnp%n_ang(i) - 1 loc = j DO k = j + 1, nnp%n_ang(i) @@ -946,7 +1671,6 @@ CONTAINS loc = k END IF END DO - ! swap symfnct CALL nnp_swapang(nnp%ang(i), j, loc) END DO END DO @@ -1046,35 +1770,37 @@ CONTAINS END SUBROUTINE nnp_swapang ! ************************************************************************************************** -!> \brief Initialize symmetry function groups -!> \param nnp ... -!> \date 2020-10-10 +!> \brief Pack symmetry functions into groups that share input parameters. +!> Builds nnp%rad(i)%symfgrp(:) and nnp%ang(i)%symfgrp(:) so that all +!> radial / angular SFs with identical (eta, rcut [, lambda, zeta]) +!> live in one group; downstream the descriptor pass evaluates each +!> group's shared cutoff/exponent once and applies it across the +!> per-element neighbour slab. +!> \param nnp NNP environment with rad/ang SF metadata already populated by nnp_init_model. !> \author Christoph Schran (christoph.schran@rub.de) ! ************************************************************************************************** SUBROUTINE nnp_init_acsf_groups(nnp) TYPE(nnp_type), INTENT(INOUT) :: nnp - INTEGER :: ang, i, j, k, rad, s - REAL(KIND=dp) :: funccut + INTEGER :: ang, i, izeta_tmp, j, k, m, n_symf, rad, & + s, sf + REAL(KIND=dp) :: eta_tmp, funccut, zeta_tmp - !find out how many symmetry functions groups are needed DO i = 1, nnp%n_ele nnp%rad(i)%n_symfgrp = 0 nnp%ang(i)%n_symfgrp = 0 - !search radial symmetry functions DO j = 1, nnp%n_ele funccut = -1.0_dp DO s = 1, nnp%n_rad(i) IF (nnp%rad(i)%ele(s) == nnp%ele(j)) THEN - IF (ABS(nnp%rad(i)%funccut(s) - funccut) > 1.0e-5_dp) THEN + IF (ABS(nnp%rad(i)%funccut(s) - funccut) > cutoff_eq_tol) THEN nnp%rad(i)%n_symfgrp = nnp%rad(i)%n_symfgrp + 1 funccut = nnp%rad(i)%funccut(s) END IF END IF END DO END DO - !search angular symmetry functions DO j = 1, nnp%n_ele DO k = j, nnp%n_ele funccut = -1.0_dp @@ -1083,7 +1809,7 @@ CONTAINS nnp%ang(i)%ele2(s) == nnp%ele(k)) .OR. & (nnp%ang(i)%ele1(s) == nnp%ele(k) .AND. & nnp%ang(i)%ele2(s) == nnp%ele(j))) THEN - IF (ABS(nnp%ang(i)%funccut(s) - funccut) > 1.0e-5_dp) THEN + IF (ABS(nnp%ang(i)%funccut(s) - funccut) > cutoff_eq_tol) THEN nnp%ang(i)%n_symfgrp = nnp%ang(i)%n_symfgrp + 1 funccut = nnp%ang(i)%funccut(s) END IF @@ -1093,7 +1819,6 @@ CONTAINS END DO END DO - !allocate memory for symmetry functions group DO i = 1, nnp%n_ele ALLOCATE (nnp%rad(i)%symfgrp(nnp%rad(i)%n_symfgrp)) ALLOCATE (nnp%ang(i)%symfgrp(nnp%ang(i)%n_symfgrp)) @@ -1105,7 +1830,6 @@ CONTAINS END DO END DO - !init symmetry functions group DO i = 1, nnp%n_ele rad = 0 ang = 0 @@ -1113,7 +1837,7 @@ CONTAINS funccut = -1.0_dp DO s = 1, nnp%n_rad(i) IF (nnp%rad(i)%ele(s) == nnp%ele(j)) THEN - IF (ABS(nnp%rad(i)%funccut(s) - funccut) > 1.0e-5_dp) THEN + IF (ABS(nnp%rad(i)%funccut(s) - funccut) > cutoff_eq_tol) THEN rad = rad + 1 funccut = nnp%rad(i)%funccut(s) nnp%rad(i)%symfgrp(rad)%cutoff = funccut @@ -1134,7 +1858,7 @@ CONTAINS nnp%ang(i)%ele2(s) == nnp%ele(k)) .OR. & (nnp%ang(i)%ele1(s) == nnp%ele(k) .AND. & nnp%ang(i)%ele2(s) == nnp%ele(j))) THEN - IF (ABS(nnp%ang(i)%funccut(s) - funccut) > 1.0e-5_dp) THEN + IF (ABS(nnp%ang(i)%funccut(s) - funccut) > cutoff_eq_tol) THEN ang = ang + 1 funccut = nnp%ang(i)%funccut(s) nnp%ang(i)%symfgrp(ang)%cutoff = funccut @@ -1152,14 +1876,13 @@ CONTAINS END DO END DO - !add symmetry functions to associated groups DO i = 1, nnp%n_ele DO j = 1, nnp%rad(i)%n_symfgrp ALLOCATE (nnp%rad(i)%symfgrp(j)%symf(nnp%rad(i)%symfgrp(j)%n_symf)) rad = 0 DO s = 1, nnp%n_rad(i) IF (nnp%rad(i)%ele(s) == nnp%rad(i)%symfgrp(j)%ele(1)) THEN - IF (ABS(nnp%rad(i)%funccut(s) - nnp%rad(i)%symfgrp(j)%cutoff) <= 1.0e-5_dp) THEN + IF (ABS(nnp%rad(i)%funccut(s) - nnp%rad(i)%symfgrp(j)%cutoff) <= cutoff_eq_tol) THEN rad = rad + 1 nnp%rad(i)%symfgrp(j)%symf(rad) = s END IF @@ -1174,7 +1897,7 @@ CONTAINS nnp%ang(i)%ele2(s) == nnp%ang(i)%symfgrp(j)%ele(2)) .OR. & (nnp%ang(i)%ele1(s) == nnp%ang(i)%symfgrp(j)%ele(2) .AND. & nnp%ang(i)%ele2(s) == nnp%ang(i)%symfgrp(j)%ele(1))) THEN - IF (ABS(nnp%ang(i)%funccut(s) - nnp%ang(i)%symfgrp(j)%cutoff) <= 1.0e-5_dp) THEN + IF (ABS(nnp%ang(i)%funccut(s) - nnp%ang(i)%symfgrp(j)%cutoff) <= cutoff_eq_tol) THEN ang = ang + 1 nnp%ang(i)%symfgrp(j)%symf(ang) = s END IF @@ -1183,14 +1906,48 @@ CONTAINS END DO END DO + ! Populate packed parameter arrays for the angular SF group inner loop + ! so it reads contiguous memory rather than chasing + ! ang(i)%{eta,zeta,lam,prefzeta}(symf(sf)) on every iteration. + ! pack_use_int_zeta and pack_izeta let the inner loop skip NINT and + ! dispatch a tight integer power when zeta is integral. The radial + ! group does not need packed parameters since nnp_calc_rad reads + ! pretabulated splines. + DO i = 1, nnp%n_ele + DO j = 1, nnp%ang(i)%n_symfgrp + n_symf = nnp%ang(i)%symfgrp(j)%n_symf + ALLOCATE (nnp%ang(i)%symfgrp(j)%pack_eta(n_symf)) + ALLOCATE (nnp%ang(i)%symfgrp(j)%pack_zeta(n_symf)) + ALLOCATE (nnp%ang(i)%symfgrp(j)%pack_lam(n_symf)) + ALLOCATE (nnp%ang(i)%symfgrp(j)%pack_prefzeta(n_symf)) + ALLOCATE (nnp%ang(i)%symfgrp(j)%pack_izeta(n_symf)) + ALLOCATE (nnp%ang(i)%symfgrp(j)%pack_use_int_zeta(n_symf)) + DO sf = 1, n_symf + m = nnp%ang(i)%symfgrp(j)%symf(sf) + eta_tmp = nnp%ang(i)%eta(m) + zeta_tmp = nnp%ang(i)%zeta(m) + nnp%ang(i)%symfgrp(j)%pack_eta(sf) = eta_tmp + nnp%ang(i)%symfgrp(j)%pack_zeta(sf) = zeta_tmp + nnp%ang(i)%symfgrp(j)%pack_lam(sf) = nnp%ang(i)%lam(m) + nnp%ang(i)%symfgrp(j)%pack_prefzeta(sf) = nnp%ang(i)%prefzeta(m) + izeta_tmp = NINT(zeta_tmp) + nnp%ang(i)%symfgrp(j)%pack_izeta(sf) = izeta_tmp + nnp%ang(i)%symfgrp(j)%pack_use_int_zeta(sf) = & + (REAL(izeta_tmp, dp) == zeta_tmp) + END DO + END DO + END DO + END SUBROUTINE nnp_init_acsf_groups ! ************************************************************************************************** -!> \brief Write symmetry function information -!> \param nnp ... -!> \param para_env ... -!> \param printtag ... -!> \date 2020-10-10 +!> \brief Print a summary of the active symmetry-function set on the source rank. +!> Emits one line per element listing the per-element n_rad / n_ang counts +!> and the per-group breakdown; used at NNP init time and after sort/group +!> passes for traceability. +!> \param nnp NNP environment whose SF metadata is to be printed. +!> \param para_env parallel environment; only the source rank emits output. +!> \param printtag log-line prefix (typically "NNP" or "HELIUM_NNP"). !> \author Christoph Schran (christoph.schran@rub.de) ! ************************************************************************************************** SUBROUTINE nnp_write_acsf(nnp, para_env, printtag) @@ -1227,188 +1984,6 @@ CONTAINS END DO END IF - RETURN - END SUBROUTINE nnp_write_acsf -! ************************************************************************************************** -!> \brief Create neighbor object -!> \param nnp ... -!> \param ind ... -!> \param nat ... -!> \param neighbor ... -!> \date 2020-10-10 -!> \author Christoph Schran (christoph.schran@rub.de) -! ************************************************************************************************** - SUBROUTINE nnp_neighbor_create(nnp, ind, nat, neighbor) - - TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp - INTEGER, INTENT(IN) :: ind, nat - TYPE(nnp_neighbor_type), INTENT(INOUT) :: neighbor - - INTEGER :: n - TYPE(cell_type), POINTER :: cell - - NULLIFY (cell) - CALL nnp_env_get(nnp_env=nnp, cell=cell) - - CALL nnp_compute_pbc_copies(neighbor%pbc_copies, cell, nnp%max_cut) - n = (SUM(neighbor%pbc_copies) + 1)*nat - ALLOCATE (neighbor%dist_rad(4, n, nnp%rad(ind)%n_symfgrp)) - ALLOCATE (neighbor%dist_ang1(4, n, nnp%ang(ind)%n_symfgrp)) - ALLOCATE (neighbor%dist_ang2(4, n, nnp%ang(ind)%n_symfgrp)) - ALLOCATE (neighbor%ind_rad(n, nnp%rad(ind)%n_symfgrp)) - ALLOCATE (neighbor%ind_ang1(n, nnp%ang(ind)%n_symfgrp)) - ALLOCATE (neighbor%ind_ang2(n, nnp%ang(ind)%n_symfgrp)) - ALLOCATE (neighbor%n_rad(nnp%rad(ind)%n_symfgrp)) - ALLOCATE (neighbor%n_ang1(nnp%ang(ind)%n_symfgrp)) - ALLOCATE (neighbor%n_ang2(nnp%ang(ind)%n_symfgrp)) - neighbor%n_rad(:) = 0 - neighbor%n_ang1(:) = 0 - neighbor%n_ang2(:) = 0 - - END SUBROUTINE nnp_neighbor_create - -! ************************************************************************************************** -!> \brief Destroy neighbor object -!> \param neighbor ... -!> \date 2020-10-10 -!> \author Christoph Schran (christoph.schran@rub.de) -! ************************************************************************************************** - SUBROUTINE nnp_neighbor_release(neighbor) - - TYPE(nnp_neighbor_type), INTENT(INOUT) :: neighbor - - DEALLOCATE (neighbor%dist_rad) - DEALLOCATE (neighbor%dist_ang1) - DEALLOCATE (neighbor%dist_ang2) - DEALLOCATE (neighbor%ind_rad) - DEALLOCATE (neighbor%ind_ang1) - DEALLOCATE (neighbor%ind_ang2) - DEALLOCATE (neighbor%n_rad) - DEALLOCATE (neighbor%n_ang1) - DEALLOCATE (neighbor%n_ang2) - - END SUBROUTINE nnp_neighbor_release - -! ************************************************************************************************** -!> \brief Generate neighboring list for an atomic configuration -!> \param nnp ... -!> \param neighbor ... -!> \param i ... -!> \date 2020-10-10 -!> \author Christoph Schran (christoph.schran@rub.de) -! ************************************************************************************************** - SUBROUTINE nnp_compute_neighbors(nnp, neighbor, i) - - TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp - TYPE(nnp_neighbor_type), INTENT(INOUT) :: neighbor - INTEGER, INTENT(IN) :: i - - INTEGER :: c1, c2, c3, ind, j, s - INTEGER, DIMENSION(3) :: nl - REAL(KIND=dp) :: norm - REAL(KIND=dp), DIMENSION(3) :: dr - TYPE(cell_type), POINTER :: cell - - NULLIFY (cell) - CALL nnp_env_get(nnp_env=nnp, cell=cell) - - ind = nnp%ele_ind(i) - - DO j = 1, nnp%num_atoms - DO c1 = 1, 2*neighbor%pbc_copies(1) + 1 - nl(1) = -neighbor%pbc_copies(1) + c1 - 1 - DO c2 = 1, 2*neighbor%pbc_copies(2) + 1 - nl(2) = -neighbor%pbc_copies(2) + c2 - 1 - DO c3 = 1, 2*neighbor%pbc_copies(3) + 1 - nl(3) = -neighbor%pbc_copies(3) + c3 - 1 - IF (j == i .AND. nl(1) == 0 .AND. nl(2) == 0 .AND. nl(3) == 0) CYCLE - dr(:) = nnp%coord(:, i) - nnp%coord(:, j) - !Apply pbc, but subtract nl boxes from periodic image - dr = pbc(dr, cell, nl) - norm = NORM2(dr(:)) - DO s = 1, nnp%rad(ind)%n_symfgrp - IF (nnp%ele_ind(j) == nnp%rad(ind)%symfgrp(s)%ele_ind(1)) THEN - IF (norm < nnp%rad(ind)%symfgrp(s)%cutoff) THEN - neighbor%n_rad(s) = neighbor%n_rad(s) + 1 - neighbor%ind_rad(neighbor%n_rad(s), s) = j - neighbor%dist_rad(1:3, neighbor%n_rad(s), s) = dr(:) - neighbor%dist_rad(4, neighbor%n_rad(s), s) = norm - END IF - END IF - END DO - DO s = 1, nnp%ang(ind)%n_symfgrp - IF (norm < nnp%ang(ind)%symfgrp(s)%cutoff) THEN - IF (nnp%ele_ind(j) == nnp%ang(ind)%symfgrp(s)%ele_ind(1)) THEN - neighbor%n_ang1(s) = neighbor%n_ang1(s) + 1 - neighbor%ind_ang1(neighbor%n_ang1(s), s) = j - neighbor%dist_ang1(1:3, neighbor%n_ang1(s), s) = dr(:) - neighbor%dist_ang1(4, neighbor%n_ang1(s), s) = norm - END IF - IF (nnp%ele_ind(j) == nnp%ang(ind)%symfgrp(s)%ele_ind(2)) THEN - neighbor%n_ang2(s) = neighbor%n_ang2(s) + 1 - neighbor%ind_ang2(neighbor%n_ang2(s), s) = j - neighbor%dist_ang2(1:3, neighbor%n_ang2(s), s) = dr(:) - neighbor%dist_ang2(4, neighbor%n_ang2(s), s) = norm - END IF - END IF - END DO - END DO - END DO - END DO - END DO - - END SUBROUTINE nnp_compute_neighbors - -! ************************************************************************************************** -!> \brief Determine required pbc copies for small cells -!> \param pbc_copies ... -!> \param cell ... -!> \param cutoff ... -!> \date 2020-10-10 -!> \author Christoph Schran (christoph.schran@rub.de) -! ************************************************************************************************** - SUBROUTINE nnp_compute_pbc_copies(pbc_copies, cell, cutoff) - INTEGER, DIMENSION(3), INTENT(INOUT) :: pbc_copies - TYPE(cell_type), INTENT(IN), POINTER :: cell - REAL(KIND=dp), INTENT(IN) :: cutoff - - REAL(KIND=dp) :: proja, projb, projc - REAL(KIND=dp), DIMENSION(3) :: axb, axc, bxc - - axb(1) = cell%hmat(2, 1)*cell%hmat(3, 2) - cell%hmat(3, 1)*cell%hmat(2, 2) - axb(2) = cell%hmat(3, 1)*cell%hmat(1, 2) - cell%hmat(1, 1)*cell%hmat(3, 2) - axb(3) = cell%hmat(1, 1)*cell%hmat(2, 2) - cell%hmat(2, 1)*cell%hmat(1, 2) - axb(:) = axb(:)/NORM2(axb(:)) - - axc(1) = cell%hmat(2, 1)*cell%hmat(3, 3) - cell%hmat(3, 1)*cell%hmat(2, 3) - axc(2) = cell%hmat(3, 1)*cell%hmat(1, 3) - cell%hmat(1, 1)*cell%hmat(3, 3) - axc(3) = cell%hmat(1, 1)*cell%hmat(2, 3) - cell%hmat(2, 1)*cell%hmat(1, 3) - axc(:) = axc(:)/NORM2(axc(:)) - - bxc(1) = cell%hmat(2, 2)*cell%hmat(3, 3) - cell%hmat(3, 2)*cell%hmat(2, 3) - bxc(2) = cell%hmat(3, 2)*cell%hmat(1, 3) - cell%hmat(1, 2)*cell%hmat(3, 3) - bxc(3) = cell%hmat(1, 2)*cell%hmat(2, 3) - cell%hmat(2, 2)*cell%hmat(1, 3) - bxc(:) = bxc(:)/NORM2(bxc(:)) - - proja = ABS(SUM(cell%hmat(:, 1)*bxc(:)))*0.5_dp - projb = ABS(SUM(cell%hmat(:, 2)*axc(:)))*0.5_dp - projc = ABS(SUM(cell%hmat(:, 3)*axb(:)))*0.5_dp - - pbc_copies(:) = 0 - DO WHILE ((pbc_copies(1) + 1)*proja <= cutoff) - pbc_copies(1) = pbc_copies(1) + 1 - END DO - DO WHILE ((pbc_copies(2) + 1)*projb <= cutoff) - pbc_copies(2) = pbc_copies(2) + 1 - END DO - DO WHILE ((pbc_copies(3) + 1)*projc <= cutoff) - pbc_copies(3) = pbc_copies(3) + 1 - END DO - ! Apply non periodic setting - pbc_copies(:) = pbc_copies(:)*cell%perd(:) - - END SUBROUTINE nnp_compute_pbc_copies - END MODULE nnp_acsf From 9592e4e09948e73cb369cc90633279738ecaa714 Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Tue, 16 Jun 2026 17:28:38 +0100 Subject: [PATCH 3/8] NNP: sparse per-neighbour force scatter; route the helium-NNP coupling through it Replace the dense dsymdxyz slab with nnp_scatter_dgdr_to_forces over the per-element dG/dr workspace, and call the same helper from the path-integral helium-solute coupling so the two force paths stay aligned. nnp_gradients now returns its result via INTENT(OUT). --- src/motion/helium_interactions.F | 35 ++--- src/nnp_force.F | 221 +++++++++++++++++++++++++------ src/nnp_model.F | 4 +- 3 files changed, 197 insertions(+), 63 deletions(-) diff --git a/src/motion/helium_interactions.F b/src/motion/helium_interactions.F index d987d46f6f..b6031c7508 100644 --- a/src/motion/helium_interactions.F +++ b/src/motion/helium_interactions.F @@ -34,8 +34,10 @@ MODULE helium_interactions USE input_section_types, ONLY: section_vals_get_subs_vals,& section_vals_type USE kinds, ONLY: dp - USE nnp_acsf, ONLY: nnp_calc_acsf + USE nnp_acsf, ONLY: nnp_calc_acsf,& + nnp_prepare_neighbor_cache USE nnp_environment_types, ONLY: nnp_type + USE nnp_force, ONLY: nnp_scatter_dgdr_to_forces USE nnp_model, ONLY: nnp_gradients,& nnp_predict USE physcon, ONLY: angstrom,& @@ -682,10 +684,8 @@ CONTAINS OPTIONAL, POINTER :: force INTEGER :: i, i_com, ig, ind, ind_he, j, k, m - LOGICAL :: extrapolate - REAL(KIND=dp) :: rsqr, rvect(3), threshold + REAL(KIND=dp) :: rsqr, rvect(3) REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: denergydsym - REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :) :: dsymdxyz TYPE(cp_logger_type), POINTER :: logger TYPE(nnp_type), POINTER :: nnp TYPE(section_vals_type), POINTER :: print_section @@ -697,9 +697,6 @@ CONTAINS helium%nnp%myforce(:, :, :) = 0.0_dp END IF - extrapolate = .FALSE. - threshold = 0.0001d0 - !fill coord array ig = 1 DO i = 1, helium%nnp%n_ele @@ -739,6 +736,8 @@ CONTAINS ! reset flag if there's an extrapolation to report: helium%nnp%output_expol = .FALSE. + nnp => helium%nnp + CALL nnp_prepare_neighbor_cache(nnp) ! calc atomic contribution to energy and force !NOTE corresponds to nnp_force line with parallelization: @@ -750,15 +749,12 @@ CONTAINS !reset input nodes and grads of ele(ind): helium%nnp%arc(ind)%layer(1)%node(:) = 0.0_dp - nnp => helium%nnp ! work around wrong INTENT of nnp_calc_acsf IF (PRESENT(force)) THEN helium%nnp%arc(ind)%layer(1)%node_grad(:) = 0.0_dp - ALLOCATE (dsymdxyz(3, helium%nnp%arc(ind)%n_nodes(1), helium%nnp%num_atoms)) ALLOCATE (denergydsym(helium%nnp%arc(ind)%n_nodes(1))) - dsymdxyz(:, :, :) = 0.0_dp - CALL nnp_calc_acsf(nnp, i, dsymdxyz) + CALL nnp_calc_acsf(nnp, i, .TRUE.) ELSE - CALL nnp_calc_acsf(nnp, i) + CALL nnp_calc_acsf(nnp, i, .FALSE.) END IF ! input nodes filled, perform prediction: @@ -771,16 +767,12 @@ CONTAINS IF (PRESENT(force)) THEN denergydsym(:) = 0.0_dp - CALL nnp_gradients(helium%nnp%arc(ind), helium%nnp, i_com, denergydsym) - DO j = 1, helium%nnp%arc(ind)%n_nodes(1) - DO k = 1, helium%nnp%num_atoms - DO m = 1, 3 - helium%nnp%myforce(m, k, i_com) = helium%nnp%myforce(m, k, i_com) & - - denergydsym(j)*dsymdxyz(m, j, k) - END DO - END DO - END DO + + ! Per-element workspace dGdr scatter; shared with the main NNP + ! force path (see nnp_scatter_dgdr_to_forces in nnp_force.F). + CALL nnp_scatter_dgdr_to_forces(nnp, ind, i, denergydsym, & + helium%nnp%myforce(:, :, i_com)) END IF END DO ! end loop over committee members @@ -788,7 +780,6 @@ CONTAINS !deallocate memory IF (PRESENT(force)) THEN DEALLOCATE (denergydsym) - DEALLOCATE (dsymdxyz) END IF END DO ! end loop over num_atoms diff --git a/src/nnp_force.F b/src/nnp_force.F index eff0d5d557..afcb7c5692 100644 --- a/src/nnp_force.F +++ b/src/nnp_force.F @@ -8,6 +8,7 @@ ! ************************************************************************************************** !> \brief Methods dealing with Neural Network potentials !> \author Christoph Schran (christoph.schran@rub.de) +!> \author Dhruv Sharma (ds2173@cam.ac.uk) !> \date 2020-10-10 ! ************************************************************************************************** MODULE nnp_force @@ -29,7 +30,8 @@ MODULE nnp_force USE kinds, ONLY: default_path_length,& default_string_length,& dp - USE nnp_acsf, ONLY: nnp_calc_acsf + USE nnp_acsf, ONLY: nnp_calc_acsf,& + nnp_prepare_neighbor_cache USE nnp_environment_types, ONLY: nnp_env_get,& nnp_type USE nnp_model, ONLY: nnp_gradients,& @@ -44,10 +46,10 @@ MODULE nnp_force PRIVATE - LOGICAL, PARAMETER, PRIVATE :: debug_this_module = .TRUE. + LOGICAL, PARAMETER, PRIVATE :: debug_this_module = .FALSE. CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'nnp_force' - PUBLIC :: nnp_calc_energy_force + PUBLIC :: nnp_calc_energy_force, nnp_scatter_dgdr_to_forces CONTAINS @@ -64,12 +66,13 @@ CONTAINS CHARACTER(len=*), PARAMETER :: routineN = 'nnp_calc_energy_force' - INTEGER :: handle, i, i_com, ig, ind, istart, j, k, & - m, mecalc + INTEGER :: handle, handle_loop, i, i_com, ig, ind, & + istart, j, k, m, max_input_nodes, & + mecalc, n_input_nodes INTEGER, ALLOCATABLE, DIMENSION(:) :: allcalc LOGICAL :: calc_stress REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: denergydsym - REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :) :: dsymdxyz, stress + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :, :) :: stress TYPE(atomic_kind_type), DIMENSION(:), POINTER :: atomic_kind_set TYPE(cp_logger_type), POINTER :: logger TYPE(cp_subsys_type), POINTER :: subsys @@ -85,6 +88,7 @@ CONTAINS logger => cp_get_default_logger() CPASSERT(ASSOCIATED(nnp)) + CPASSERT(nnp%n_committee >= 1) CALL nnp_env_get(nnp_env=nnp, particle_set=particle_set, & subsys=subsys, local_particles=local_particles, & atomic_kind_set=atomic_kind_set) @@ -93,9 +97,9 @@ CONTAINS virial=virial) calc_stress = virial%pv_availability .AND. (.NOT. virial%pv_numer) - IF (calc_stress .AND. .NOT. calc_forces) CPABORT('Stress cannot be calculated without forces') + IF (calc_stress .AND. .NOT. calc_forces) & + CPABORT('Stress cannot be calculated without forces') - ! Initialize energy and gradient: nnp%atomic_energy(:, :) = 0.0_dp IF (calc_forces) nnp%myforce(:, :, :) = 0.0_dp IF (calc_forces) nnp%committee_forces(:, :, :) = 0.0_dp @@ -134,11 +138,34 @@ CONTAINS ! reset extrapolation status nnp%output_expol = .FALSE. + CALL nnp_prepare_neighbor_cache(nnp) + + max_input_nodes = 0 + IF (calc_forces) THEN + DO i = 1, nnp%n_ele + max_input_nodes = MAX(max_input_nodes, nnp%arc(i)%n_nodes(1)) + END DO + ! Per-neighbour dGdr lives in the per-element workspace; the force scatter + ! walks the per-(ind) neighbour lists directly, with no global + ! (3, n_sf, num_atoms) slab. + IF (calc_stress) THEN + ALLOCATE (stress(3, 3, max_input_nodes)) + stress(:, :, :) = 0.0_dp + END IF + ALLOCATE (denergydsym(max_input_nodes)) + denergydsym(:) = 0.0_dp + END IF + + ! Per-rank timer around the per-atom work. CP2K aggregates the label as + ! avg/max across ranks, so max - avg measures the load imbalance. + CALL timeset('nnp_per_rank_atom_loop', handle_loop) + ! calc atomic contribution to energy and force DO i = istart, istart + mecalc - 1 ! determine index of atom type and offset ind = nnp%ele_ind(i) + n_input_nodes = nnp%arc(ind)%n_nodes(1) ! reset input nodes of ele(ind): nnp%arc(ind)%layer(1)%node(:) = 0.0_dp @@ -147,17 +174,14 @@ CONTAINS IF (calc_forces) THEN !reset input grads of ele(ind): nnp%arc(ind)%layer(1)%node_grad(:) = 0.0_dp - ALLOCATE (dsymdxyz(3, nnp%arc(ind)%n_nodes(1), nnp%num_atoms)) - dsymdxyz(:, :, :) = 0.0_dp IF (calc_stress) THEN - ALLOCATE (stress(3, 3, nnp%arc(ind)%n_nodes(1))) - stress(:, :, :) = 0.0_dp - CALL nnp_calc_acsf(nnp, i, dsymdxyz, stress) + stress(:, :, 1:n_input_nodes) = 0.0_dp + CALL nnp_calc_acsf(nnp, i, .TRUE., stress(:, :, 1:n_input_nodes)) ELSE - CALL nnp_calc_acsf(nnp, i, dsymdxyz) + CALL nnp_calc_acsf(nnp, i, .TRUE.) END IF ELSE - CALL nnp_calc_acsf(nnp, i) + CALL nnp_calc_acsf(nnp, i, .FALSE.) END IF DO i_com = 1, nnp%n_committee @@ -168,34 +192,35 @@ CONTAINS ! predict forces IF (calc_forces) THEN - ALLOCATE (denergydsym(nnp%arc(ind)%n_nodes(1))) - denergydsym(:) = 0.0_dp - CALL nnp_gradients(nnp%arc(ind), nnp, i_com, denergydsym) - DO j = 1, nnp%arc(ind)%n_nodes(1) - DO k = 1, nnp%num_atoms - DO m = 1, 3 - nnp%myforce(m, k, i_com) = nnp%myforce(m, k, i_com) - denergydsym(j)*dsymdxyz(m, j, k) - END DO - END DO - IF (calc_stress) THEN + denergydsym(1:n_input_nodes) = 0.0_dp + CALL nnp_gradients(nnp%arc(ind), nnp, i_com, denergydsym(1:n_input_nodes)) + + ! Force scatter over the per-element workspace dGdr arrays; see + ! nnp_scatter_dgdr_to_forces below, also reused by the helium-NNP + ! coupling in src/motion/helium_interactions.F. + CALL nnp_scatter_dgdr_to_forces(nnp, ind, i, denergydsym(1:n_input_nodes), & + nnp%myforce(:, :, i_com)) + + IF (calc_stress) THEN + DO j = 1, n_input_nodes nnp%committee_stress(:, :, i_com) = nnp%committee_stress(:, :, i_com) - & denergydsym(j)*stress(:, :, j) - END IF - END DO - DEALLOCATE (denergydsym) + END DO + END IF END IF END DO - !deallocate memory - IF (calc_forces) THEN - DEALLOCATE (dsymdxyz) - IF (calc_stress) THEN - DEALLOCATE (stress) - END IF - END IF - END DO ! loop over num_atoms + CALL timestop(handle_loop) + + IF (calc_forces) THEN + DEALLOCATE (denergydsym) + IF (calc_stress) THEN + DEALLOCATE (stress) + END IF + END IF + ! calculate energy: CALL logger%para_env%sum(nnp%atomic_energy(:, :)) nnp%committee_energy(:) = SUM(nnp%atomic_energy, 1) @@ -335,7 +360,7 @@ CONTAINS print_key => section_vals_get_subs_vals(print_section, "FORCES") IF (BTEST(cp_print_key_should_output(logger%iter_info, print_key), cp_p_file)) THEN - IF (unit_nr > 0) CALL nnp_print_forces(nnp, print_key) + CALL nnp_print_forces(nnp, print_key) END IF print_key => section_vals_get_subs_vals(print_section, "FORCES_SIGMA") @@ -426,7 +451,6 @@ CONTAINS NULLIFY (logger) logger => cp_get_default_logger() - ! TODO use write_particle_coordinates from particle_methods.F DO i = 1, nnp%n_committee WRITE (fmt_string, *) i WRITE (middle_name, "(A,A)") "nnp-forces-", ADJUSTL(TRIM(fmt_string)) @@ -612,7 +636,6 @@ CONTAINS CHARACTER(len=default_path_length) :: fmt_string INTEGER :: i - ! TODO use write_particle_coordinates from particle_methods.F WRITE (unit_nr, *) nnp%num_atoms WRITE (unit_nr, "(A,F20.9)") "NNP bias forces [a.u.] for bias energy [a.u]=", nnp%bias_energy @@ -671,4 +694,124 @@ CONTAINS END SUBROUTINE nnp_print_sumforces +! ************************************************************************************************** +!> \brief Scatter the per-neighbour dG/dr arrays held in the nnp_neighbor_workspace +!> into a per-atom Cartesian force array. Three contributions per central +!> atom i: +!> 1) self: self_dGdr(:, m) -> force_xyz(:, i) +!> 2) radial: walk neighbor%rad(s)%ind(j), read dGdr_rad(s)%data(:, sf, j) +!> 3) angular: walk neighbor%ang1(s)%ind / ang2(s)%ind, read dGdr_ang_jj/kk +!> No global (n_sf, num_atoms) slab is touched. Reused by the helium-NNP +!> coupling in helium_interactions. +!> +!> Precondition: nnp_calc_acsf(nnp, i, calc_forces=.TRUE.[, stress]) must +!> have run for this same atom i immediately before; the per-element +!> workspace it fills is overwritten on every ACSF call. +!> \param nnp ... +!> \param ind central-atom element index +!> \param i central atom index (absolute) +!> \param denergydsym dE/dG_k for k = 1..n_input_nodes(ind) +!> \param force_xyz (3, num_atoms) destination -- accumulated, NOT overwritten +!> \author Dhruv Sharma (ds2173@cam.ac.uk) +! ************************************************************************************************** + SUBROUTINE nnp_scatter_dgdr_to_forces(nnp, ind, i, denergydsym, force_xyz) + + TYPE(nnp_type), INTENT(IN) :: nnp + INTEGER, INTENT(IN) :: ind, i + REAL(KIND=dp), DIMENSION(:), INTENT(IN) :: denergydsym + REAL(KIND=dp), DIMENSION(:, :), INTENT(INOUT) :: force_xyz + + INTEGER :: j, k, k_atom, m, n_ang1_s, n_ang2_s, & + n_input_nodes, n_symf_s, off, s, sf + LOGICAL :: homo_grp + REAL(KIND=dp) :: de + + ASSOCIATE (workspace => nnp%neighbor_interface_state%workspace(ind), & + neighbor => nnp%neighbor_interface_state%workspace(ind)%neighbor, & + self_dGdr => nnp%neighbor_interface_state%workspace(ind)%self_dGdr) + + n_input_nodes = workspace%n_input_nodes + + ! Self contribution + DO j = 1, n_input_nodes + de = denergydsym(j) + force_xyz(1, i) = force_xyz(1, i) - de*self_dGdr(1, j) + force_xyz(2, i) = force_xyz(2, i) - de*self_dGdr(2, j) + force_xyz(3, i) = force_xyz(3, i) - de*self_dGdr(3, j) + END DO + + ! Radial neighbours + DO s = 1, nnp%rad(ind)%n_symfgrp + n_symf_s = nnp%rad(ind)%symfgrp(s)%n_symf + ASSOCIATE (rad_buf => workspace%dGdr_rad(s)%data) + DO j = 1, neighbor%n_rad(s) + k_atom = neighbor%rad(s)%ind(j) + DO sf = 1, n_symf_s + m = nnp%rad(ind)%symfgrp(s)%symf(sf) + de = denergydsym(m) + force_xyz(1, k_atom) = force_xyz(1, k_atom) - de*rad_buf(1, sf, j) + force_xyz(2, k_atom) = force_xyz(2, k_atom) - de*rad_buf(2, sf, j) + force_xyz(3, k_atom) = force_xyz(3, k_atom) - de*rad_buf(3, sf, j) + END DO + END DO + END ASSOCIATE + END DO + + ! Angular neighbours + off = nnp%n_rad(ind) + DO s = 1, nnp%ang(ind)%n_symfgrp + n_symf_s = nnp%ang(ind)%symfgrp(s)%n_symf + n_ang1_s = neighbor%n_ang1(s) + homo_grp = (nnp%ang(ind)%symfgrp(s)%ele(1) == nnp%ang(ind)%symfgrp(s)%ele(2)) + IF (homo_grp) THEN + n_ang2_s = n_ang1_s ! kk buffer is also indexed in ang1(s)%ind + ELSE + n_ang2_s = neighbor%n_ang2(s) + END IF + + ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & + kk_buf => workspace%dGdr_ang_kk(s)%data) + ! jj-side + DO j = 1, n_ang1_s + k_atom = neighbor%ang1(s)%ind(j) + DO sf = 1, n_symf_s + m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) + de = denergydsym(m) + force_xyz(1, k_atom) = force_xyz(1, k_atom) - de*jj_buf(1, sf, j) + force_xyz(2, k_atom) = force_xyz(2, k_atom) - de*jj_buf(2, sf, j) + force_xyz(3, k_atom) = force_xyz(3, k_atom) - de*jj_buf(3, sf, j) + END DO + END DO + + ! kk-side: homo reads ang1(s)%ind, hetero reads ang2(s)%ind + IF (homo_grp) THEN + DO k = 1, n_ang2_s + k_atom = neighbor%ang1(s)%ind(k) + DO sf = 1, n_symf_s + m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) + de = denergydsym(m) + force_xyz(1, k_atom) = force_xyz(1, k_atom) - de*kk_buf(1, sf, k) + force_xyz(2, k_atom) = force_xyz(2, k_atom) - de*kk_buf(2, sf, k) + force_xyz(3, k_atom) = force_xyz(3, k_atom) - de*kk_buf(3, sf, k) + END DO + END DO + ELSE + DO k = 1, n_ang2_s + k_atom = neighbor%ang2(s)%ind(k) + DO sf = 1, n_symf_s + m = off + nnp%ang(ind)%symfgrp(s)%symf(sf) + de = denergydsym(m) + force_xyz(1, k_atom) = force_xyz(1, k_atom) - de*kk_buf(1, sf, k) + force_xyz(2, k_atom) = force_xyz(2, k_atom) - de*kk_buf(2, sf, k) + force_xyz(3, k_atom) = force_xyz(3, k_atom) - de*kk_buf(3, sf, k) + END DO + END DO + END IF + END ASSOCIATE + END DO + + END ASSOCIATE + + END SUBROUTINE nnp_scatter_dgdr_to_forces + END MODULE nnp_force diff --git a/src/nnp_model.F b/src/nnp_model.F index 27fd798162..04d8051f2a 100644 --- a/src/nnp_model.F +++ b/src/nnp_model.F @@ -28,7 +28,7 @@ MODULE nnp_model PRIVATE - LOGICAL, PARAMETER, PRIVATE :: debug_this_module = .TRUE. + LOGICAL, PARAMETER, PRIVATE :: debug_this_module = .FALSE. CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'nnp_model' PUBLIC :: nnp_write_arc, & @@ -169,7 +169,7 @@ CONTAINS TYPE(nnp_arc_type), INTENT(INOUT) :: arc TYPE(nnp_type), INTENT(INOUT) :: nnp INTEGER, INTENT(IN) :: i_com - REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: denergydsym + REAL(KIND=dp), DIMENSION(:), INTENT(OUT) :: denergydsym CHARACTER(len=*), PARAMETER :: routineN = 'nnp_gradients' From 10a550c3619c4a85289ddd84493443ed421b4915 Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Tue, 16 Jun 2026 17:28:38 +0100 Subject: [PATCH 4/8] NNP: expose RAD_SPLINE_N and VERLET_SKIN input keywords Add the two tuning keywords to the main &NNP section and the helium-solute &NNP section, read onto the nnp environment. Defaults reproduce the previous behaviour (RAD_SPLINE_N 8192; VERLET_SKIN auto = MIN(0.5 bohr, 0.1*cutoff)). --- src/input_cp2k_nnp.F | 49 +++++++- src/nnp_environment.F | 203 ++++++++++++++++++++++++---------- src/start/input_cp2k_motion.F | 37 ++++++- 3 files changed, 227 insertions(+), 62 deletions(-) diff --git a/src/input_cp2k_nnp.F b/src/input_cp2k_nnp.F index f93b3ddba5..9480d167f0 100644 --- a/src/input_cp2k_nnp.F +++ b/src/input_cp2k_nnp.F @@ -8,6 +8,7 @@ ! ************************************************************************************************** !> \brief Creates the NNP section of the input !> \author Christoph Schran (christoph.schran@rub.de) +!> \author Dhruv Sharma (ds2173@cam.ac.uk) !> \date 2020-10-10 ! ************************************************************************************************** MODULE input_cp2k_nnp @@ -28,6 +29,7 @@ MODULE input_cp2k_nnp section_release,& section_type USE input_val_types, ONLY: char_t,& + integer_t,& real_t USE kinds, ONLY: dp #include "./base/base_uses.f90" @@ -35,7 +37,7 @@ MODULE input_cp2k_nnp IMPLICIT NONE PRIVATE - LOGICAL, PRIVATE, PARAMETER :: debug_this_module = .TRUE. + LOGICAL, PRIVATE, PARAMETER :: debug_this_module = .FALSE. CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'input_cp2k_nnp' PUBLIC :: create_nnp_section @@ -58,7 +60,7 @@ CONTAINS CALL section_create(section, __LOCATION__, name="NNP", & description="This section contains all information to run a "// & "Neural Network Potential (NNP) calculation.", & - n_keywords=3, n_subsections=3, repeats=.FALSE., & + n_keywords=5, n_subsections=3, repeats=.FALSE., & citations=[Behler2007, Behler2011, Schran2020a, Schran2020b]) NULLIFY (subsection, subsubsection, keyword) @@ -76,6 +78,49 @@ CONTAINS CALL section_add_keyword(section, keyword) CALL keyword_release(keyword) + CALL keyword_create(keyword, __LOCATION__, name="RAD_SPLINE_N", & + description="Number of knots per radial group in "// & + "the cubic-Hermite spline tables that tabulate "// & + "the radial symmetry-function product "// & + "y(r) = exp(-eta*(r-rs)^2) * fcut(r). "// & + "The value error scales as O(1/n^4) and the "// & + "force (derivative) error as O(1/n^3); the "// & + "default keeps both inside the NNP regression "// & + "tolerance for a radial cutoff of ~12 bohr "// & + "(value ~1e-14, force ~1e-10). Larger cutoffs or "// & + "stricter tolerances may need a larger n, with "// & + "the force term the binding constraint. Memory "// & + "cost scales linearly in n per group.", & + repeats=.FALSE., & + n_var=1, & + type_of_var=integer_t, & + default_i_val=8192, & + usage="RAD_SPLINE_N 8192") + CALL section_add_keyword(section, keyword) + CALL keyword_release(keyword) + + CALL keyword_create(keyword, __LOCATION__, name="VERLET_SKIN", & + description="Verlet skin distance for the NNP descriptor "// & + "neighbour cell-list. The neighbour-list cutoff is "// & + "(symmetry-function cutoff + skin); the cell-list "// & + "chain is rebuilt only when an atom drifts more "// & + "than skin/2 from its rebuild-time position, "// & + "analogous to the LAMMPS 'neighbor bin' "// & + "command. Larger skin reduces the rebuild rate "// & + "but enlarges the per-atom neighbour list. "// & + "A negative value (default) selects the "// & + "automatic heuristic MIN(0.5 bohr, 0.1*cutoff). "// & + "Useful upper bound is half the smallest "// & + "perpendicular cell width.", & + repeats=.FALSE., & + n_var=1, & + type_of_var=real_t, & + default_r_val=-1.0_dp, & + unit_str="bohr", & + usage="VERLET_SKIN [bohr] 0.5") + CALL section_add_keyword(section, keyword) + CALL keyword_release(keyword) + ! BIAS subsection CALL section_create(subsection, __LOCATION__, name="BIAS", & description="Section to bias the committee disagreement (sigma) by "// & diff --git a/src/nnp_environment.F b/src/nnp_environment.F index 8471909b09..0a1100d53f 100644 --- a/src/nnp_environment.F +++ b/src/nnp_environment.F @@ -8,6 +8,7 @@ ! ************************************************************************************************** !> \brief Methods dealing with Neural Network potentials !> \author Christoph Schran (christoph.schran@rub.de) +!> \author Dhruv Sharma (ds2173@cam.ac.uk) !> \date 2020-10-10 ! ************************************************************************************************** MODULE nnp_environment @@ -63,7 +64,7 @@ MODULE nnp_environment PRIVATE - LOGICAL, PARAMETER, PRIVATE :: debug_this_module = .TRUE. + LOGICAL, PARAMETER, PRIVATE :: debug_this_module = .FALSE. CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'nnp_environment' PUBLIC :: nnp_init @@ -231,20 +232,16 @@ CONTAINS CHARACTER(LEN=*), INTENT(IN) :: printtag CHARACTER(len=*), PARAMETER :: routineN = 'nnp_init_model' - INTEGER, PARAMETER :: def_str_len = 256, & - default_path_length = 256 + INTEGER, PARAMETER :: def_str_len = 256 CHARACTER(len=1), ALLOCATABLE, DIMENSION(:) :: cactfnct CHARACTER(len=2) :: ele CHARACTER(len=def_str_len) :: dummy, line - CHARACTER(len=default_path_length) :: base_name, file_name - INTEGER :: handle, i, i_com, io, iweight, j, k, l, & - n_weight, nele, nuc_ele, symfnct_type, & - unit_nr - LOGICAL :: at_end, atom_e_found, explicit, first, & - found + CHARACTER(len=default_path_length) :: file_name + INTEGER :: handle, i, io, j, k, nele, nuc_ele, & + symfnct_type, unit_nr + LOGICAL :: atom_e_found, explicit, first, found REAL(KIND=dp) :: energy - REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: weights REAL(KIND=dp), DIMENSION(7) :: test_array REAL(KIND=dp), DIMENSION(:), POINTER :: work TYPE(cp_logger_type), POINTER :: logger @@ -272,6 +269,11 @@ CONTAINS ALLOCATE (nnp_env%committee_stress(3, 3, nnp_env%n_committee)) CALL section_vals_val_get(nnp_env%nnp_input, "NNP_INPUT_FILE_NAME", c_val=file_name) + CALL section_vals_val_get(nnp_env%nnp_input, "RAD_SPLINE_N", i_val=nnp_env%rad_spline_n) + IF (nnp_env%rad_spline_n < 2) THEN + CPABORT("NNP| RAD_SPLINE_N must be >= 2.") + END IF + CALL section_vals_val_get(nnp_env%nnp_input, "VERLET_SKIN", r_val=nnp_env%verlet_skin) CALL parser_create(parser, file_name, para_env=logger%para_env) ! read number of elements and cut_type and check for scale and center @@ -306,10 +308,12 @@ CONTAINS search_from_begin_of_file=.TRUE.) nnp_env%scale_acsf = found - ! Test if there are two keywords of this: + ! parser_search_string matches substrings, so a search for + ! "scale_symmetry_functions" also fires on "..._sigma". Re-search from the + ! current parser position to confirm the bare keyword appears on its own line. CALL parser_search_string(parser, "scale_symmetry_functions", .TRUE., found) IF (found .AND. nnp_env%scale_sigma_acsf) THEN - CPWARN('Two scaling keywords in the input, we will ignore sigma scaling in this case') + CPWARN("Ignoring sigma ACSF scaling; both keywords were set.") nnp_env%scale_sigma_acsf = .FALSE. ELSE IF (.NOT. found .AND. nnp_env%scale_sigma_acsf) THEN nnp_env%scale_acsf = .FALSE. @@ -689,11 +693,59 @@ CONTAINS END DO END DO CALL parser_release(parser) + + ! Reject degenerate scaling.data at load time. The (loc_max - loc_min) + ! and sigma denominators are used unguarded inside nnp_scale_acsf; + ! a zero range or zero sigma there produces silent NaN forces. + IF (nnp_env%scale_acsf) THEN + DO i = 1, nnp_env%n_ele + DO j = 1, nnp_env%n_rad(i) + IF (nnp_env%rad(i)%loc_max(j) <= nnp_env%rad(i)%loc_min(j)) THEN + WRITE (line, '(A,I0,A,I0,A,2(1X,ES13.6))') & + "scaling.data: radial sf range non-positive for element ", i, & + " sf ", j, " (loc_min, loc_max) =", & + nnp_env%rad(i)%loc_min(j), nnp_env%rad(i)%loc_max(j) + CPABORT(TRIM(line)) + END IF + END DO + DO j = 1, nnp_env%n_ang(i) + IF (nnp_env%ang(i)%loc_max(j) <= nnp_env%ang(i)%loc_min(j)) THEN + WRITE (line, '(A,I0,A,I0,A,2(1X,ES13.6))') & + "scaling.data: angular sf range non-positive for element ", i, & + " sf ", j, " (loc_min, loc_max) =", & + nnp_env%ang(i)%loc_min(j), nnp_env%ang(i)%loc_max(j) + CPABORT(TRIM(line)) + END IF + END DO + END DO + END IF + IF (nnp_env%scale_sigma_acsf) THEN + DO i = 1, nnp_env%n_ele + DO j = 1, nnp_env%n_rad(i) + IF (nnp_env%rad(i)%sigma(j) <= 0.0_dp) THEN + WRITE (line, '(A,I0,A,I0,A,1X,ES13.6)') & + "scaling.data: radial sf sigma non-positive for element ", i, & + " sf ", j, " sigma =", nnp_env%rad(i)%sigma(j) + CPABORT(TRIM(line)) + END IF + END DO + DO j = 1, nnp_env%n_ang(i) + IF (nnp_env%ang(i)%sigma(j) <= 0.0_dp) THEN + WRITE (line, '(A,I0,A,I0,A,1X,ES13.6)') & + "scaling.data: angular sf sigma non-positive for element ", i, & + " sf ", j, " sigma =", nnp_env%ang(i)%sigma(j) + CPABORT(TRIM(line)) + END IF + END DO + END DO + END IF END IF CALL nnp_init_acsf_groups(nnp_env) - ! read weights from file + ! Allocate per-layer weight tables, then load each committee member + ! from disk. Allocation lives at the call site because n_committee / + ! n_layer / n_nodes are owned here; the loader only fills them. DO i = 1, nnp_env%n_ele DO j = 2, nnp_env%n_layer ALLOCATE (nnp_env%arc(i)%layer(j)%weights(nnp_env%arc(i)%n_nodes(j - 1), & @@ -701,6 +753,84 @@ CONTAINS ALLOCATE (nnp_env%arc(i)%layer(j)%bweights(nnp_env%arc(i)%n_nodes(j), nnp_env%n_committee)) END DO END DO + CALL nnp_read_committee_weights(nnp_env, model_section, printtag) + + nnp_env%expol = 0 + + ! Bias the standard deviation of committee disagreement + NULLIFY (bias_section) + explicit = .FALSE. + !HELIUM NNP does not currently define a bias term + bias_section => section_vals_get_subs_vals(nnp_env%nnp_input, "BIAS", can_return_null=.TRUE.) + IF (ASSOCIATED(bias_section)) CALL section_vals_get(bias_section, explicit=explicit) + nnp_env%bias = .FALSE. + IF (explicit) THEN + IF (nnp_env%n_committee > 1) THEN + IF (logger%para_env%is_source()) THEN + WRITE (unit_nr, *) "NNP| Biasing of committee disagreement enabled" + END IF + nnp_env%bias = .TRUE. + ALLOCATE (nnp_env%bias_forces(3, nnp_env%num_atoms)) + ALLOCATE (nnp_env%bias_e_avrg(nnp_env%n_committee)) + CALL section_vals_val_get(bias_section, "SIGMA_0", r_val=nnp_env%bias_sigma0) + CALL section_vals_val_get(bias_section, "K_B", r_val=nnp_env%bias_kb) + nnp_env%bias_e_avrg(:) = 0.0_dp + CALL section_vals_val_get(bias_section, "ALIGN_NNP_ENERGIES", explicit=explicit) + nnp_env%bias_align = explicit + IF (explicit) THEN + NULLIFY (work) + CALL section_vals_val_get(bias_section, "ALIGN_NNP_ENERGIES", r_vals=work) + IF (SIZE(work) /= nnp_env%n_committee) THEN + CPABORT("ALIGN_NNP_ENERGIES size mismatch wrt committee size.") + END IF + nnp_env%bias_e_avrg(:) = work + IF (logger%para_env%is_source()) THEN + WRITE (unit_nr, *) TRIM(printtag)//"| Biasing is aligned by shifting the energy prediction of the C-NNP members" + END IF + END IF + ELSE + CPWARN("NNP committee size is 1, BIAS section is ignored.") + END IF + END IF + + IF (logger%para_env%is_source()) THEN + WRITE (unit_nr, *) TRIM(printtag)//"| NNP force environment initialized" + END IF + + CALL timestop(handle) + + END SUBROUTINE nnp_init_model + +! ************************************************************************************************** +!> \brief Read committee weights from disk into pre-allocated arc layer tables. +!> +!> One file per (committee member, element), named ..data. The file +!> is a flat list of doubles; the order on disk is layer-major over (i->j weights +!> then biases) so a single counter walks every value into its slot. +!> +!> \param nnp_env NNP environment with arc()%layer()%weights/bweights already allocated. +!> \param model_section &MODEL section block; supplies the per-member WEIGHTS base path. +!> \param printtag Log-line prefix. +! ************************************************************************************************** + SUBROUTINE nnp_read_committee_weights(nnp_env, model_section, printtag) + TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp_env + TYPE(section_vals_type), POINTER :: model_section + CHARACTER(LEN=*), INTENT(IN) :: printtag + + CHARACTER(len=default_path_length) :: base_name, file_name + INTEGER :: i, i_com, iweight, j, k, l, n_weight, & + unit_nr + LOGICAL :: at_end + REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: weights + TYPE(cp_logger_type), POINTER :: logger + TYPE(cp_parser_type) :: parser + + NULLIFY (logger) + logger => cp_get_default_logger() + IF (logger%para_env%is_source()) THEN + unit_nr = cp_logger_get_default_unit_nr(logger) + END IF + DO i_com = 1, nnp_env%n_committee CALL section_vals_val_get(model_section, "WEIGHTS", c_val=base_name, i_rep_section=i_com) IF (logger%para_env%is_source()) THEN @@ -748,51 +878,6 @@ CONTAINS END DO END DO - !Initialize extrapolation counter - nnp_env%expol = 0 - - ! Bias the standard deviation of committee disagreement - NULLIFY (bias_section) - explicit = .FALSE. - !HELIUM NNP does atm not allow for bias (not even defined) - bias_section => section_vals_get_subs_vals(nnp_env%nnp_input, "BIAS", can_return_null=.TRUE.) - IF (ASSOCIATED(bias_section)) CALL section_vals_get(bias_section, explicit=explicit) - nnp_env%bias = .FALSE. - IF (explicit) THEN - IF (nnp_env%n_committee > 1) THEN - IF (logger%para_env%is_source()) THEN - WRITE (unit_nr, *) "NNP| Biasing of committee disagreement enabled" - END IF - nnp_env%bias = .TRUE. - ALLOCATE (nnp_env%bias_forces(3, nnp_env%num_atoms)) - ALLOCATE (nnp_env%bias_e_avrg(nnp_env%n_committee)) - CALL section_vals_val_get(bias_section, "SIGMA_0", r_val=nnp_env%bias_sigma0) - CALL section_vals_val_get(bias_section, "K_B", r_val=nnp_env%bias_kb) - nnp_env%bias_e_avrg(:) = 0.0_dp - CALL section_vals_val_get(bias_section, "ALIGN_NNP_ENERGIES", explicit=explicit) - nnp_env%bias_align = explicit - IF (explicit) THEN - NULLIFY (work) - CALL section_vals_val_get(bias_section, "ALIGN_NNP_ENERGIES", r_vals=work) - IF (SIZE(work) /= nnp_env%n_committee) THEN - CPABORT("ALIGN_NNP_ENERGIES size mismatch wrt committee size.") - END IF - nnp_env%bias_e_avrg(:) = work - IF (logger%para_env%is_source()) THEN - WRITE (unit_nr, *) TRIM(printtag)//"| Biasing is aligned by shifting the energy prediction of the C-NNP members" - END IF - END IF - ELSE - CPWARN("NNP committee size is 1, BIAS section is ignored.") - END IF - END IF - - IF (logger%para_env%is_source()) THEN - WRITE (unit_nr, *) TRIM(printtag)//"| NNP force environment initialized" - END IF - - CALL timestop(handle) - - END SUBROUTINE nnp_init_model + END SUBROUTINE nnp_read_committee_weights END MODULE nnp_environment diff --git a/src/start/input_cp2k_motion.F b/src/start/input_cp2k_motion.F index 550678a73f..d2aee7aece 100644 --- a/src/start/input_cp2k_motion.F +++ b/src/start/input_cp2k_motion.F @@ -2334,7 +2334,7 @@ CONTAINS CALL section_create(subsection, __LOCATION__, name="NNP", & description="This section contains all information to run an helium-solute "// & "interaction Neural Network Potential (NNP) calculation.", & - n_keywords=2, n_subsections=3, repeats=.FALSE.) + n_keywords=4, n_subsections=3, repeats=.FALSE.) CALL keyword_create(keyword, __LOCATION__, name="NNP_INPUT_FILE_NAME", & description="File containing the input information for the setup "// & @@ -2350,6 +2350,41 @@ CONTAINS CALL section_add_keyword(subsection, keyword) CALL keyword_release(keyword) + CALL keyword_create(keyword, __LOCATION__, name="RAD_SPLINE_N", & + description="Number of knots per radial group in the cubic-Hermite "// & + "spline tables that tabulate the radial symmetry-function product "// & + "y(r) = exp(-eta*(r-rs)^2) * fcut(r). Cubic-Hermite per-evaluation "// & + "error scales as O(1/n^4); the default keeps the spline residual "// & + "inside the NNP regression tolerance for a radial cutoff of "// & + "~12 bohr. Models with larger cutoffs or stricter tolerances may "// & + "need a larger n. Memory cost scales linearly in n per radial group.", & + repeats=.FALSE., & + n_var=1, & + type_of_var=integer_t, & + default_i_val=8192, & + usage="RAD_SPLINE_N 8192") + CALL section_add_keyword(subsection, keyword) + CALL keyword_release(keyword) + + CALL keyword_create(keyword, __LOCATION__, name="VERLET_SKIN", & + description="Verlet skin distance for the NNP descriptor neighbour "// & + "cell-list. The neighbour-list cutoff is (symmetry-function cutoff "// & + "+ skin); the cell-list chain is rebuilt only when an atom drifts "// & + "more than skin/2 from its rebuild-time position, analogous to the "// & + "LAMMPS 'neighbor bin' command. Larger skin reduces the "// & + "rebuild rate but enlarges the per-atom neighbour list. A negative "// & + "value (default) selects the automatic heuristic "// & + "MIN(0.5 bohr, 0.1*cutoff). Useful upper bound is half the smallest "// & + "perpendicular cell width.", & + repeats=.FALSE., & + n_var=1, & + type_of_var=real_t, & + default_r_val=-1.0_dp, & + unit_str="bohr", & + usage="VERLET_SKIN [bohr] 0.5") + CALL section_add_keyword(subsection, keyword) + CALL keyword_release(keyword) + NULLIFY (subsubsection) CALL section_create(subsubsection, __LOCATION__, name="SR_CUTOFF", & description="Section for failsafe short range cutoffs for the NNPs, "// & From 4b2c0ebd816ee240539bdb02f037de2335dc703b Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Tue, 16 Jun 2026 17:28:38 +0100 Subject: [PATCH 5/8] NNP: bit-exact MD regtest, test matchers, and user documentation Add an H2O-256 bit-exact MD regression test locking step-0 energy and the conserved quantity, a first-line matcher flag on GenericMatcher, and the docs/methods/machine_learning/nnp.md user page. --- docs/methods/machine_learning/nnp.md | 80 +- tests/HUGE_TESTS_SUPPRESSIONS | 4 + .../regtest-1/H2O-256_C-NNP_MD-bitexact.inp | 851 ++++++++++++++++++ tests/NNP/regtest-1/TEST_FILES.toml | 14 + tests/matcher_classes.py | 11 +- tests/matchers.py | 9 + 6 files changed, 965 insertions(+), 4 deletions(-) create mode 100644 tests/NNP/regtest-1/H2O-256_C-NNP_MD-bitexact.inp diff --git a/docs/methods/machine_learning/nnp.md b/docs/methods/machine_learning/nnp.md index 22a4232655..aef4a553d9 100644 --- a/docs/methods/machine_learning/nnp.md +++ b/docs/methods/machine_learning/nnp.md @@ -1,8 +1,84 @@ # Neural Network Potentials -Unfortunately no one has gotten around to writing this page yet :-( +CP2K supports Behler-Parrinello high-dimensional neural network potentials (HDNNPs) with +atom-centred symmetry functions (ACSF) as descriptors. NNP can drive any CP2K run that goes through +`FORCE_EVAL`, from single-point energies through geometry optimisation, molecular dynamics, biased +MD via free-energy methods, and committee-of-models extrapolation diagnostics. The same NNP path +also backs the helium-solvent interaction in path-integral runs. -In the meantime, the following links might be helpful: +The implementation reads networks trained with the [n2p2](https://github.com/CompPhysVienna/n2p2) +format (`input.nn`, `scaling.data`, `weights..data`). + +## Input + +The NNP method is selected with `METHOD NNP` inside `FORCE_EVAL`. The network files and one or more +model definitions are configured under `&NNP`. The complete option list is generated from the source +and is linked from the input reference manual. + +A minimal committee-NNP MD input looks like + +``` +&FORCE_EVAL + METHOD NNP + &NNP + NNP_INPUT_FILE_NAME nnp-1/input.nn + SCALE_FILE_NAME nnp-1/scaling.data + &MODEL + WEIGHTS nnp-1/weights + &END MODEL + &MODEL + WEIGHTS nnp-2/weights + &END MODEL + ! ... up to 8 typical for committee error bars ... + &END NNP + &SUBSYS + &CELL + ABC [angstrom] 12.42 12.42 12.42 + &END CELL + &COORD + ! ... + &END COORD + &END SUBSYS +&END FORCE_EVAL +``` + +Worked examples covering NVT, NPT, biased MD, and restarts live under +[`tests/NNP/regtest-1/`](https://github.com/cp2k/cp2k/tree/master/tests/NNP/regtest-1). The +path-integral helium-solute coupling is exercised by +[`tests/Pimd/regtest-2/water_in_helium_nnp.inp`](https://github.com/cp2k/cp2k/tree/master/tests/Pimd/regtest-2). + +## Tuning + +The default ACSF spline grid (`RAD_SPLINE_N 8192`) is calibrated for radial cutoffs around 12 bohr. +Models with substantially larger cutoffs or stricter accuracy targets may want a larger value. The +cubic-Hermite value residual scales as O(1/n^4) and the force (derivative) residual as O(1/n^3); at +the default grid both stay near machine precision for a 12 bohr cutoff (value error ~1e-14, force +error ~1e-10), with the force term the binding constraint as the cutoff grows. + +The cell-list neighbour search uses a Verlet skin so the chain is only rebuilt when an atom drifts +more than `skin/2` between force evaluations. By default the skin auto-selects +`MIN(0.5 bohr, 0.1 * cutoff)`. Long stable trajectories can raise it to reduce the rebuild rate at +the cost of a larger per-atom neighbour list: + +``` +&NNP + ! ... + VERLET_SKIN [bohr] 1.0 +&END NNP +``` + +This is the analogue of LAMMPS's `neighbor bin` command. A negative value (the default) +selects the automatic heuristic; useful upper bounds sit near half the smallest perpendicular cell +width. See the input reference manual for the full list of NNP tuning keywords. + +## Parallelism + +NNP supports both MPI and OpenMP parallelism. MPI distributes the atoms across ranks, and OpenMP +parallelises the per-atom descriptor and force loops inside each rank. + +## References + +For background on the method and the existing implementation, the following links might be helpful - - diff --git a/tests/HUGE_TESTS_SUPPRESSIONS b/tests/HUGE_TESTS_SUPPRESSIONS index d97e544c38..d9452d8d12 100644 --- a/tests/HUGE_TESTS_SUPPRESSIONS +++ b/tests/HUGE_TESTS_SUPPRESSIONS @@ -167,4 +167,8 @@ QS/regtest-smeagol-2/au-h2-au-1x1x1-1V-forces.inp # Tests produced 11.69 MiB of output. QS/regtest-openpmd/H2O-geo-ot-lumo-all-openpmd.inp +# Test produced 9.87 MiB of output (per-step &FORCES print x 8 committee +# models x finite-difference stress perturbations). +NNP/regtest-1/H2O-64_C-NNP_MD-NpT-numeric.inp + #EOF diff --git a/tests/NNP/regtest-1/H2O-256_C-NNP_MD-bitexact.inp b/tests/NNP/regtest-1/H2O-256_C-NNP_MD-bitexact.inp new file mode 100644 index 0000000000..27ca4919b5 --- /dev/null +++ b/tests/NNP/regtest-1/H2O-256_C-NNP_MD-bitexact.inp @@ -0,0 +1,851 @@ +&GLOBAL + PRINT_LEVEL LOW + PROJECT H2O-256_C-NNP_MD-bitexact + RUN_TYPE MD +&END GLOBAL + +&MOTION + &MD + ENSEMBLE NVT + STEPS 5 + TEMPERATURE 300 + TIMESTEP 0.5 + &PRINT + FORCE_LAST + &ENERGY + &EACH + MD 50 + &END EACH + &END ENERGY + &END PRINT + &THERMOSTAT + REGION GLOBAL + TYPE CSVR + &CSVR + TIMECON 30.00 + &END CSVR + &END THERMOSTAT + &END MD + &PRINT + &TRAJECTORY + &EACH + MD 500 + &END EACH + &END TRAJECTORY + &END PRINT +&END MOTION + +&FORCE_EVAL + METHOD NNP + &NNP + NNP_INPUT_FILE_NAME NNP/bulkH2O-jcp2020-cnnp/nnp-1/input.nn + SCALE_FILE_NAME NNP/bulkH2O-jcp2020-cnnp/nnp-1/scaling.data + &MODEL + WEIGHTS NNP/bulkH2O-jcp2020-cnnp/nnp-1/weights + &END MODEL + &MODEL + WEIGHTS NNP/bulkH2O-jcp2020-cnnp/nnp-2/weights + &END MODEL + &MODEL + WEIGHTS NNP/bulkH2O-jcp2020-cnnp/nnp-3/weights + &END MODEL + &MODEL + WEIGHTS NNP/bulkH2O-jcp2020-cnnp/nnp-4/weights + &END MODEL + &MODEL + WEIGHTS NNP/bulkH2O-jcp2020-cnnp/nnp-5/weights + &END MODEL + &MODEL + WEIGHTS NNP/bulkH2O-jcp2020-cnnp/nnp-6/weights + &END MODEL + &MODEL + WEIGHTS NNP/bulkH2O-jcp2020-cnnp/nnp-7/weights + &END MODEL + &MODEL + WEIGHTS NNP/bulkH2O-jcp2020-cnnp/nnp-8/weights + &END MODEL + &PRINT + &ENERGIES + &EACH + MD 100 + &END EACH + &END ENERGIES + &END PRINT + &END NNP + &SUBSYS + &CELL + ABC [angstrom] 24.84000000 24.84000000 12.42000000 + PERIODIC XYZ + &END CELL + &COORD + O 17.85470000 14.89360000 4.30373000 + H 18.81710000 14.81910000 4.30668000 + H 17.60460000 14.06810000 4.71635000 + O 5.08473000 -2.09187000 -12.52020000 + H 5.79642000 -1.44510000 -12.70560000 + H 5.42957000 -2.91898000 -12.19440000 + O 4.02996000 7.60544000 -3.68524000 + H 4.27003000 6.94768000 -4.41446000 + H 4.77458000 8.28889000 -3.67208000 + O -12.22650000 18.89220000 12.25860000 + H -13.08640000 19.19600000 12.55890000 + H -12.45140000 18.59980000 11.38190000 + O 1.36461000 4.68750000 16.07470000 + H 0.77220000 5.33188000 15.70890000 + H 0.92914100 4.08655000 16.73020000 + O -3.42189000 3.99641000 12.57220000 + H -2.47934000 4.05205000 12.38670000 + H -3.57781000 4.18579000 13.49940000 + O -12.45810000 5.26919000 -2.78020000 + H -11.67410000 4.62996000 -2.73820000 + H -13.15450000 4.83976000 -3.28648000 + O -12.87460000 14.22990000 10.97500000 + H -12.40850000 13.48810000 11.29410000 + H -12.17020000 14.92310000 10.79950000 + O 16.21660000 10.92160000 2.09580000 + H 15.77660000 10.15310000 2.51719000 + H 16.70080000 10.54170000 1.31827000 + O 10.17360000 12.92940000 3.09205000 + H 10.77360000 12.63090000 3.73607000 + H 9.56179000 12.13220000 2.94022000 + O 7.77605000 4.98249000 -2.29855000 + H 8.07228000 4.60108000 -1.47153000 + H 6.99714000 5.40667000 -2.04022000 + O 14.58280000 2.43851000 2.33359000 + H 13.85980000 2.38788000 1.70989000 + H 14.40950000 3.36726000 2.62982000 + O -6.66475000 9.71604000 21.73940000 + H -6.28708000 10.17630000 20.91490000 + H -5.93185000 9.37252000 22.22920000 + O 1.00730000 7.62893000 -3.71784000 + H 1.95122000 7.70838000 -3.49284000 + H 0.78209600 6.70589000 -3.39957000 + O -2.00359000 8.81074000 4.64761000 + H -2.33580000 9.40933000 3.96349000 + H -1.02960000 8.88476000 4.52207000 + O 12.25120000 10.23950000 9.10079000 + H 12.66980000 10.95650000 8.67039000 + H 12.71420000 9.38279000 8.86118000 + O -0.24441300 15.84350000 13.25460000 + H 0.00470418 16.62720000 12.75340000 + H -0.42289200 15.22620000 12.52560000 + O 17.25350000 -6.21439000 18.95750000 + H 17.26240000 -7.18011000 18.92590000 + H 16.44650000 -5.96877000 18.52680000 + O 1.57232000 8.55013000 3.48535000 + H 1.78649000 8.35399000 4.39869000 + H 0.91633100 7.79040000 3.31060000 + O 9.73687000 10.53440000 -12.33500000 + H 9.33966000 11.43670000 -12.37350000 + H 10.71770000 10.62350000 -12.51260000 + O 3.19186000 -1.70399000 6.69143000 + H 2.64086000 -0.91080900 6.70096000 + H 3.29213000 -1.99342000 7.64120000 + O 5.01286000 3.41556000 7.01155000 + H 5.25540000 3.30163000 7.95343000 + H 5.82406000 3.47012000 6.52344000 + O 16.21560000 -0.86663700 -2.85105000 + H 17.05640000 -1.46010000 -2.85949000 + H 15.82330000 -0.94576600 -1.95729000 + O 18.39240000 15.45230000 9.37872000 + H 17.70210000 15.50250000 10.02480000 + H 19.03780000 16.17220000 9.59111000 + O 12.43740000 -8.71873000 -6.55194000 + H 13.24560000 -9.18214000 -6.25474000 + H 11.74430000 -9.43587000 -6.45253000 + O -2.12768000 -4.80632000 13.63960000 + H -1.96159000 -3.92263000 13.27480000 + H -3.14023000 -4.92484000 13.58540000 + O 5.47695000 -12.78860000 18.12950000 + H 6.04948000 -12.92690000 18.88220000 + H 4.74944000 -13.43130000 18.27050000 + O 9.06380000 13.52840000 12.92680000 + H 9.07296000 14.42340000 12.64730000 + H 9.45712000 13.49050000 13.82760000 + O 6.54756000 -11.95970000 12.30680000 + H 5.90365000 -11.48740000 12.84550000 + H 7.46789000 -11.66100000 12.52900000 + O -3.25432000 -2.19516000 15.18330000 + H -3.21119000 -2.38945000 14.23370000 + H -4.18029000 -2.00474000 15.49650000 + O 19.16120000 20.64270000 4.17300000 + H 18.25990000 20.42210000 4.00929000 + H 19.17920000 21.59520000 4.03867000 + O 16.62900000 4.75612000 3.61679000 + H 17.41650000 4.29018000 3.87743000 + H 15.90400000 4.43119000 4.10392000 + O -14.46860000 1.26932000 -6.11559000 + H -13.90410000 0.58683400 -6.47353000 + H -14.78000000 1.12280000 -5.19548000 + O 2.42216000 12.91590000 4.10819000 + H 1.94154000 13.51420000 3.57260000 + H 2.79204000 12.34870000 3.45220000 + O 17.30610000 14.63850000 1.43734000 + H 16.39600000 14.75480000 1.71543000 + H 17.68200000 14.67460000 2.27931000 + O 3.42317000 13.87760000 8.37823000 + H 4.23289000 13.97000000 7.90067000 + H 3.52615000 13.14510000 8.97284000 + O -4.91217000 23.61640000 20.12810000 + H -4.02584000 23.47060000 19.75830000 + H -4.83605000 24.14340000 20.97620000 + O 20.30930000 8.75645000 -1.55507000 + H 21.03230000 9.38853000 -1.42815000 + H 20.44040000 8.12820000 -2.30558000 + O 17.63270000 6.21611000 -1.45303000 + H 17.18510000 6.65247000 -2.18930000 + H 17.59600000 6.94078000 -0.77261100 + O -11.85650000 -3.09341000 6.38427000 + H -11.91870000 -3.99992000 6.73656000 + H -10.96070000 -2.82121000 6.56532000 + O 14.34220000 -5.83286000 5.75163000 + H 14.20720000 -6.54550000 5.13562000 + H 14.05200000 -6.07263000 6.61672000 + O -1.09658000 6.20827000 5.58702000 + H -2.04987000 5.94104000 5.55373000 + H -0.68250800 5.41005000 5.89003000 + O 8.13776000 2.00921000 16.81480000 + H 8.76908000 1.47311000 16.26590000 + H 8.54559000 2.38894000 17.63890000 + O 0.05855300 -0.59401100 17.45460000 + H 0.96214000 -0.31451700 17.27990000 + H 0.08964280 -1.51268000 17.79400000 + O 27.67350000 0.60729400 12.06030000 + H 28.34720000 0.76104100 12.69580000 + H 27.19490000 -0.15785700 12.45860000 + O 9.89541000 -7.95756000 8.17742000 + H 9.62897000 -8.79966000 8.58524000 + H 9.28724000 -7.36014000 8.69880000 + O -4.41370000 20.18930000 8.05879000 + H -3.76101000 20.81950000 7.70885000 + H -4.49934000 19.50700000 7.38700000 + O 16.99780000 3.63080000 11.63730000 + H 17.31810000 3.30370000 12.51290000 + H 17.08550000 4.57727000 11.68570000 + O 6.48319000 10.92640000 3.40103000 + H 5.63763000 10.99260000 2.85932000 + H 6.32943000 11.66920000 4.05745000 + O 4.49883000 7.78536000 13.14360000 + H 3.57897000 7.92015000 12.89780000 + H 4.40785000 7.58602000 14.13040000 + O 22.38010000 -2.58249000 19.81360000 + H 22.50240000 -2.69404000 18.86030000 + H 23.27000000 -2.40926000 20.13910000 + O 7.60549000 20.11670000 1.52318000 + H 7.40576000 20.44790000 2.44617000 + H 6.89393000 19.54060000 1.16986000 + O -9.80851000 15.18180000 5.82547000 + H -8.98420000 15.49160000 6.23087000 + H -9.53181000 14.61140000 5.10912000 + O 1.89878000 3.39080000 9.95468000 + H 2.53193000 3.64424000 10.67490000 + H 2.50766000 2.74971000 9.48478000 + O 9.54794000 13.95950000 -3.42910000 + H 8.80101000 13.54530000 -2.93926000 + H 10.27920000 13.98280000 -2.79475000 + O -3.92553000 -7.74473000 -9.61729000 + H -3.86420000 -6.99741000 -8.98787000 + H -3.75817000 -8.54726000 -9.04343000 + O 0.44092400 10.71270000 12.05860000 + H 0.83931100 9.90666000 12.42840000 + H 0.35887600 10.57580000 11.11070000 + O 12.12770000 6.70656000 3.12351000 + H 11.86650000 6.60036000 4.03971000 + H 11.35860000 6.98032000 2.60943000 + O 26.48550000 8.45521000 -11.83130000 + H 26.44880000 8.37541000 -10.86690000 + H 25.91510000 7.70284000 -12.22220000 + O 8.76592000 6.61206000 4.83144000 + H 9.47545000 7.33383000 4.67087000 + H 7.97602000 7.11755000 4.74199000 + O -4.62340000 16.47700000 6.19871000 + H -3.86477000 16.34810000 6.84392000 + H -4.32266000 17.35310000 5.96346000 + O 13.40050000 0.18383700 -4.66240000 + H 14.24900000 0.62818200 -4.64394000 + H 12.99810000 0.46925800 -5.47766000 + O 4.34821000 7.30338000 3.29043000 + H 3.53537000 7.62296000 3.70824000 + H 4.26206000 6.32561000 3.39956000 + O 19.59060000 -11.77550000 22.11300000 + H 19.37940000 -11.65870000 23.07720000 + H 19.24050000 -10.92170000 21.74620000 + O 17.85470000 27.31360000 4.30373000 + H 18.81710000 27.23910000 4.30668000 + H 17.60460000 26.48810000 4.71635000 + O 5.08473000 10.32813000 -12.52020000 + H 5.79642000 10.97490000 -12.70560000 + H 5.42957000 9.50102000 -12.19440000 + O 4.02996000 20.02544000 -3.68524000 + H 4.27003000 19.36768000 -4.41446000 + H 4.77458000 20.70889000 -3.67208000 + O -12.22650000 31.31220000 12.25860000 + H -13.08640000 31.61600000 12.55890000 + H -12.45140000 31.01980000 11.38190000 + O 1.36461000 17.10750000 16.07470000 + H 0.77220000 17.75188000 15.70890000 + H 0.92914100 16.50655000 16.73020000 + O -3.42189000 16.41641000 12.57220000 + H -2.47934000 16.47205000 12.38670000 + H -3.57781000 16.60579000 13.49940000 + O -12.45810000 17.68919000 -2.78020000 + H -11.67410000 17.04996000 -2.73820000 + H -13.15450000 17.25976000 -3.28648000 + O -12.87460000 26.64990000 10.97500000 + H -12.40850000 25.90810000 11.29410000 + H -12.17020000 27.34310000 10.79950000 + O 16.21660000 23.34160000 2.09580000 + H 15.77660000 22.57310000 2.51719000 + H 16.70080000 22.96170000 1.31827000 + O 10.17360000 25.34940000 3.09205000 + H 10.77360000 25.05090000 3.73607000 + H 9.56179000 24.55220000 2.94022000 + O 7.77605000 17.40249000 -2.29855000 + H 8.07228000 17.02108000 -1.47153000 + H 6.99714000 17.82667000 -2.04022000 + O 14.58280000 14.85851000 2.33359000 + H 13.85980000 14.80788000 1.70989000 + H 14.40950000 15.78726000 2.62982000 + O -6.66475000 22.13604000 21.73940000 + H -6.28708000 22.59630000 20.91490000 + H -5.93185000 21.79252000 22.22920000 + O 1.00730000 20.04893000 -3.71784000 + H 1.95122000 20.12838000 -3.49284000 + H 0.78209600 19.12589000 -3.39957000 + O -2.00359000 21.23074000 4.64761000 + H -2.33580000 21.82933000 3.96349000 + H -1.02960000 21.30476000 4.52207000 + O 12.25120000 22.65950000 9.10079000 + H 12.66980000 23.37650000 8.67039000 + H 12.71420000 21.80279000 8.86118000 + O -0.24441300 28.26350000 13.25460000 + H 0.00470418 29.04720000 12.75340000 + H -0.42289200 27.64620000 12.52560000 + O 17.25350000 6.20561000 18.95750000 + H 17.26240000 5.23989000 18.92590000 + H 16.44650000 6.45123000 18.52680000 + O 1.57232000 20.97013000 3.48535000 + H 1.78649000 20.77399000 4.39869000 + H 0.91633100 20.21040000 3.31060000 + O 9.73687000 22.95440000 -12.33500000 + H 9.33966000 23.85670000 -12.37350000 + H 10.71770000 23.04350000 -12.51260000 + O 3.19186000 10.71601000 6.69143000 + H 2.64086000 11.50919100 6.70096000 + H 3.29213000 10.42658000 7.64120000 + O 5.01286000 15.83556000 7.01155000 + H 5.25540000 15.72163000 7.95343000 + H 5.82406000 15.89012000 6.52344000 + O 16.21560000 11.55336300 -2.85105000 + H 17.05640000 10.95990000 -2.85949000 + H 15.82330000 11.47423400 -1.95729000 + O 18.39240000 27.87230000 9.37872000 + H 17.70210000 27.92250000 10.02480000 + H 19.03780000 28.59220000 9.59111000 + O 12.43740000 3.70127000 -6.55194000 + H 13.24560000 3.23786000 -6.25474000 + H 11.74430000 2.98413000 -6.45253000 + O -2.12768000 7.61368000 13.63960000 + H -1.96159000 8.49737000 13.27480000 + H -3.14023000 7.49516000 13.58540000 + O 5.47695000 -0.36860000 18.12950000 + H 6.04948000 -0.50690000 18.88220000 + H 4.74944000 -1.01130000 18.27050000 + O 9.06380000 25.94840000 12.92680000 + H 9.07296000 26.84340000 12.64730000 + H 9.45712000 25.91050000 13.82760000 + O 6.54756000 0.46030000 12.30680000 + H 5.90365000 0.93260000 12.84550000 + H 7.46789000 0.75900000 12.52900000 + O -3.25432000 10.22484000 15.18330000 + H -3.21119000 10.03055000 14.23370000 + H -4.18029000 10.41526000 15.49650000 + O 19.16120000 33.06270000 4.17300000 + H 18.25990000 32.84210000 4.00929000 + H 19.17920000 34.01520000 4.03867000 + O 16.62900000 17.17612000 3.61679000 + H 17.41650000 16.71018000 3.87743000 + H 15.90400000 16.85119000 4.10392000 + O -14.46860000 13.68932000 -6.11559000 + H -13.90410000 13.00683400 -6.47353000 + H -14.78000000 13.54280000 -5.19548000 + O 2.42216000 25.33590000 4.10819000 + H 1.94154000 25.93420000 3.57260000 + H 2.79204000 24.76870000 3.45220000 + O 17.30610000 27.05850000 1.43734000 + H 16.39600000 27.17480000 1.71543000 + H 17.68200000 27.09460000 2.27931000 + O 3.42317000 26.29760000 8.37823000 + H 4.23289000 26.39000000 7.90067000 + H 3.52615000 25.56510000 8.97284000 + O -4.91217000 36.03640000 20.12810000 + H -4.02584000 35.89060000 19.75830000 + H -4.83605000 36.56340000 20.97620000 + O 20.30930000 21.17645000 -1.55507000 + H 21.03230000 21.80853000 -1.42815000 + H 20.44040000 20.54820000 -2.30558000 + O 17.63270000 18.63611000 -1.45303000 + H 17.18510000 19.07247000 -2.18930000 + H 17.59600000 19.36078000 -0.77261100 + O -11.85650000 9.32659000 6.38427000 + H -11.91870000 8.42008000 6.73656000 + H -10.96070000 9.59879000 6.56532000 + O 14.34220000 6.58714000 5.75163000 + H 14.20720000 5.87450000 5.13562000 + H 14.05200000 6.34737000 6.61672000 + O -1.09658000 18.62827000 5.58702000 + H -2.04987000 18.36104000 5.55373000 + H -0.68250800 17.83005000 5.89003000 + O 8.13776000 14.42921000 16.81480000 + H 8.76908000 13.89311000 16.26590000 + H 8.54559000 14.80894000 17.63890000 + O 0.05855300 11.82598900 17.45460000 + H 0.96214000 12.10548300 17.27990000 + H 0.08964280 10.90732000 17.79400000 + O 27.67350000 13.02729400 12.06030000 + H 28.34720000 13.18104100 12.69580000 + H 27.19490000 12.26214300 12.45860000 + O 9.89541000 4.46244000 8.17742000 + H 9.62897000 3.62034000 8.58524000 + H 9.28724000 5.05986000 8.69880000 + O -4.41370000 32.60930000 8.05879000 + H -3.76101000 33.23950000 7.70885000 + H -4.49934000 31.92700000 7.38700000 + O 16.99780000 16.05080000 11.63730000 + H 17.31810000 15.72370000 12.51290000 + H 17.08550000 16.99727000 11.68570000 + O 6.48319000 23.34640000 3.40103000 + H 5.63763000 23.41260000 2.85932000 + H 6.32943000 24.08920000 4.05745000 + O 4.49883000 20.20536000 13.14360000 + H 3.57897000 20.34015000 12.89780000 + H 4.40785000 20.00602000 14.13040000 + O 22.38010000 9.83751000 19.81360000 + H 22.50240000 9.72596000 18.86030000 + H 23.27000000 10.01074000 20.13910000 + O 7.60549000 32.53670000 1.52318000 + H 7.40576000 32.86790000 2.44617000 + H 6.89393000 31.96060000 1.16986000 + O -9.80851000 27.60180000 5.82547000 + H -8.98420000 27.91160000 6.23087000 + H -9.53181000 27.03140000 5.10912000 + O 1.89878000 15.81080000 9.95468000 + H 2.53193000 16.06424000 10.67490000 + H 2.50766000 15.16971000 9.48478000 + O 9.54794000 26.37950000 -3.42910000 + H 8.80101000 25.96530000 -2.93926000 + H 10.27920000 26.40280000 -2.79475000 + O -3.92553000 4.67527000 -9.61729000 + H -3.86420000 5.42259000 -8.98787000 + H -3.75817000 3.87274000 -9.04343000 + O 0.44092400 23.13270000 12.05860000 + H 0.83931100 22.32666000 12.42840000 + H 0.35887600 22.99580000 11.11070000 + O 12.12770000 19.12656000 3.12351000 + H 11.86650000 19.02036000 4.03971000 + H 11.35860000 19.40032000 2.60943000 + O 26.48550000 20.87521000 -11.83130000 + H 26.44880000 20.79541000 -10.86690000 + H 25.91510000 20.12284000 -12.22220000 + O 8.76592000 19.03206000 4.83144000 + H 9.47545000 19.75383000 4.67087000 + H 7.97602000 19.53755000 4.74199000 + O -4.62340000 28.89700000 6.19871000 + H -3.86477000 28.76810000 6.84392000 + H -4.32266000 29.77310000 5.96346000 + O 13.40050000 12.60383700 -4.66240000 + H 14.24900000 13.04818200 -4.64394000 + H 12.99810000 12.88925800 -5.47766000 + O 4.34821000 19.72338000 3.29043000 + H 3.53537000 20.04296000 3.70824000 + H 4.26206000 18.74561000 3.39956000 + O 19.59060000 0.64450000 22.11300000 + H 19.37940000 0.76130000 23.07720000 + H 19.24050000 1.49830000 21.74620000 + O 30.27470000 14.89360000 4.30373000 + H 31.23710000 14.81910000 4.30668000 + H 30.02460000 14.06810000 4.71635000 + O 17.50473000 -2.09187000 -12.52020000 + H 18.21642000 -1.44510000 -12.70560000 + H 17.84957000 -2.91898000 -12.19440000 + O 16.44996000 7.60544000 -3.68524000 + H 16.69003000 6.94768000 -4.41446000 + H 17.19458000 8.28889000 -3.67208000 + O 0.19350000 18.89220000 12.25860000 + H -0.66640000 19.19600000 12.55890000 + H -0.03140000 18.59980000 11.38190000 + O 13.78461000 4.68750000 16.07470000 + H 13.19220000 5.33188000 15.70890000 + H 13.34914100 4.08655000 16.73020000 + O 8.99811000 3.99641000 12.57220000 + H 9.94066000 4.05205000 12.38670000 + H 8.84219000 4.18579000 13.49940000 + O -0.03810000 5.26919000 -2.78020000 + H 0.74590000 4.62996000 -2.73820000 + H -0.73450000 4.83976000 -3.28648000 + O -0.45460000 14.22990000 10.97500000 + H 0.01150000 13.48810000 11.29410000 + H 0.24980000 14.92310000 10.79950000 + O 28.63660000 10.92160000 2.09580000 + H 28.19660000 10.15310000 2.51719000 + H 29.12080000 10.54170000 1.31827000 + O 22.59360000 12.92940000 3.09205000 + H 23.19360000 12.63090000 3.73607000 + H 21.98179000 12.13220000 2.94022000 + O 20.19605000 4.98249000 -2.29855000 + H 20.49228000 4.60108000 -1.47153000 + H 19.41714000 5.40667000 -2.04022000 + O 27.00280000 2.43851000 2.33359000 + H 26.27980000 2.38788000 1.70989000 + H 26.82950000 3.36726000 2.62982000 + O 5.75525000 9.71604000 21.73940000 + H 6.13292000 10.17630000 20.91490000 + H 6.48815000 9.37252000 22.22920000 + O 13.42730000 7.62893000 -3.71784000 + H 14.37122000 7.70838000 -3.49284000 + H 13.20209600 6.70589000 -3.39957000 + O 10.41641000 8.81074000 4.64761000 + H 10.08420000 9.40933000 3.96349000 + H 11.39040000 8.88476000 4.52207000 + O 24.67120000 10.23950000 9.10079000 + H 25.08980000 10.95650000 8.67039000 + H 25.13420000 9.38279000 8.86118000 + O 12.17558700 15.84350000 13.25460000 + H 12.42470418 16.62720000 12.75340000 + H 11.99710800 15.22620000 12.52560000 + O 29.67350000 -6.21439000 18.95750000 + H 29.68240000 -7.18011000 18.92590000 + H 28.86650000 -5.96877000 18.52680000 + O 13.99232000 8.55013000 3.48535000 + H 14.20649000 8.35399000 4.39869000 + H 13.33633100 7.79040000 3.31060000 + O 22.15687000 10.53440000 -12.33500000 + H 21.75966000 11.43670000 -12.37350000 + H 23.13770000 10.62350000 -12.51260000 + O 15.61186000 -1.70399000 6.69143000 + H 15.06086000 -0.91080900 6.70096000 + H 15.71213000 -1.99342000 7.64120000 + O 17.43286000 3.41556000 7.01155000 + H 17.67540000 3.30163000 7.95343000 + H 18.24406000 3.47012000 6.52344000 + O 28.63560000 -0.86663700 -2.85105000 + H 29.47640000 -1.46010000 -2.85949000 + H 28.24330000 -0.94576600 -1.95729000 + O 30.81240000 15.45230000 9.37872000 + H 30.12210000 15.50250000 10.02480000 + H 31.45780000 16.17220000 9.59111000 + O 24.85740000 -8.71873000 -6.55194000 + H 25.66560000 -9.18214000 -6.25474000 + H 24.16430000 -9.43587000 -6.45253000 + O 10.29232000 -4.80632000 13.63960000 + H 10.45841000 -3.92263000 13.27480000 + H 9.27977000 -4.92484000 13.58540000 + O 17.89695000 -12.78860000 18.12950000 + H 18.46948000 -12.92690000 18.88220000 + H 17.16944000 -13.43130000 18.27050000 + O 21.48380000 13.52840000 12.92680000 + H 21.49296000 14.42340000 12.64730000 + H 21.87712000 13.49050000 13.82760000 + O 18.96756000 -11.95970000 12.30680000 + H 18.32365000 -11.48740000 12.84550000 + H 19.88789000 -11.66100000 12.52900000 + O 9.16568000 -2.19516000 15.18330000 + H 9.20881000 -2.38945000 14.23370000 + H 8.23971000 -2.00474000 15.49650000 + O 31.58120000 20.64270000 4.17300000 + H 30.67990000 20.42210000 4.00929000 + H 31.59920000 21.59520000 4.03867000 + O 29.04900000 4.75612000 3.61679000 + H 29.83650000 4.29018000 3.87743000 + H 28.32400000 4.43119000 4.10392000 + O -2.04860000 1.26932000 -6.11559000 + H -1.48410000 0.58683400 -6.47353000 + H -2.36000000 1.12280000 -5.19548000 + O 14.84216000 12.91590000 4.10819000 + H 14.36154000 13.51420000 3.57260000 + H 15.21204000 12.34870000 3.45220000 + O 29.72610000 14.63850000 1.43734000 + H 28.81600000 14.75480000 1.71543000 + H 30.10200000 14.67460000 2.27931000 + O 15.84317000 13.87760000 8.37823000 + H 16.65289000 13.97000000 7.90067000 + H 15.94615000 13.14510000 8.97284000 + O 7.50783000 23.61640000 20.12810000 + H 8.39416000 23.47060000 19.75830000 + H 7.58395000 24.14340000 20.97620000 + O 32.72930000 8.75645000 -1.55507000 + H 33.45230000 9.38853000 -1.42815000 + H 32.86040000 8.12820000 -2.30558000 + O 30.05270000 6.21611000 -1.45303000 + H 29.60510000 6.65247000 -2.18930000 + H 30.01600000 6.94078000 -0.77261100 + O 0.56350000 -3.09341000 6.38427000 + H 0.50130000 -3.99992000 6.73656000 + H 1.45930000 -2.82121000 6.56532000 + O 26.76220000 -5.83286000 5.75163000 + H 26.62720000 -6.54550000 5.13562000 + H 26.47200000 -6.07263000 6.61672000 + O 11.32342000 6.20827000 5.58702000 + H 10.37013000 5.94104000 5.55373000 + H 11.73749200 5.41005000 5.89003000 + O 20.55776000 2.00921000 16.81480000 + H 21.18908000 1.47311000 16.26590000 + H 20.96559000 2.38894000 17.63890000 + O 12.47855300 -0.59401100 17.45460000 + H 13.38214000 -0.31451700 17.27990000 + H 12.50964280 -1.51268000 17.79400000 + O 40.09350000 0.60729400 12.06030000 + H 40.76720000 0.76104100 12.69580000 + H 39.61490000 -0.15785700 12.45860000 + O 22.31541000 -7.95756000 8.17742000 + H 22.04897000 -8.79966000 8.58524000 + H 21.70724000 -7.36014000 8.69880000 + O 8.00630000 20.18930000 8.05879000 + H 8.65899000 20.81950000 7.70885000 + H 7.92066000 19.50700000 7.38700000 + O 29.41780000 3.63080000 11.63730000 + H 29.73810000 3.30370000 12.51290000 + H 29.50550000 4.57727000 11.68570000 + O 18.90319000 10.92640000 3.40103000 + H 18.05763000 10.99260000 2.85932000 + H 18.74943000 11.66920000 4.05745000 + O 16.91883000 7.78536000 13.14360000 + H 15.99897000 7.92015000 12.89780000 + H 16.82785000 7.58602000 14.13040000 + O 34.80010000 -2.58249000 19.81360000 + H 34.92240000 -2.69404000 18.86030000 + H 35.69000000 -2.40926000 20.13910000 + O 20.02549000 20.11670000 1.52318000 + H 19.82576000 20.44790000 2.44617000 + H 19.31393000 19.54060000 1.16986000 + O 2.61149000 15.18180000 5.82547000 + H 3.43580000 15.49160000 6.23087000 + H 2.88819000 14.61140000 5.10912000 + O 14.31878000 3.39080000 9.95468000 + H 14.95193000 3.64424000 10.67490000 + H 14.92766000 2.74971000 9.48478000 + O 21.96794000 13.95950000 -3.42910000 + H 21.22101000 13.54530000 -2.93926000 + H 22.69920000 13.98280000 -2.79475000 + O 8.49447000 -7.74473000 -9.61729000 + H 8.55580000 -6.99741000 -8.98787000 + H 8.66183000 -8.54726000 -9.04343000 + O 12.86092400 10.71270000 12.05860000 + H 13.25931100 9.90666000 12.42840000 + H 12.77887600 10.57580000 11.11070000 + O 24.54770000 6.70656000 3.12351000 + H 24.28650000 6.60036000 4.03971000 + H 23.77860000 6.98032000 2.60943000 + O 38.90550000 8.45521000 -11.83130000 + H 38.86880000 8.37541000 -10.86690000 + H 38.33510000 7.70284000 -12.22220000 + O 21.18592000 6.61206000 4.83144000 + H 21.89545000 7.33383000 4.67087000 + H 20.39602000 7.11755000 4.74199000 + O 7.79660000 16.47700000 6.19871000 + H 8.55523000 16.34810000 6.84392000 + H 8.09734000 17.35310000 5.96346000 + O 25.82050000 0.18383700 -4.66240000 + H 26.66900000 0.62818200 -4.64394000 + H 25.41810000 0.46925800 -5.47766000 + O 16.76821000 7.30338000 3.29043000 + H 15.95537000 7.62296000 3.70824000 + H 16.68206000 6.32561000 3.39956000 + O 32.01060000 -11.77550000 22.11300000 + H 31.79940000 -11.65870000 23.07720000 + H 31.66050000 -10.92170000 21.74620000 + O 30.27470000 27.31360000 4.30373000 + H 31.23710000 27.23910000 4.30668000 + H 30.02460000 26.48810000 4.71635000 + O 17.50473000 10.32813000 -12.52020000 + H 18.21642000 10.97490000 -12.70560000 + H 17.84957000 9.50102000 -12.19440000 + O 16.44996000 20.02544000 -3.68524000 + H 16.69003000 19.36768000 -4.41446000 + H 17.19458000 20.70889000 -3.67208000 + O 0.19350000 31.31220000 12.25860000 + H -0.66640000 31.61600000 12.55890000 + H -0.03140000 31.01980000 11.38190000 + O 13.78461000 17.10750000 16.07470000 + H 13.19220000 17.75188000 15.70890000 + H 13.34914100 16.50655000 16.73020000 + O 8.99811000 16.41641000 12.57220000 + H 9.94066000 16.47205000 12.38670000 + H 8.84219000 16.60579000 13.49940000 + O -0.03810000 17.68919000 -2.78020000 + H 0.74590000 17.04996000 -2.73820000 + H -0.73450000 17.25976000 -3.28648000 + O -0.45460000 26.64990000 10.97500000 + H 0.01150000 25.90810000 11.29410000 + H 0.24980000 27.34310000 10.79950000 + O 28.63660000 23.34160000 2.09580000 + H 28.19660000 22.57310000 2.51719000 + H 29.12080000 22.96170000 1.31827000 + O 22.59360000 25.34940000 3.09205000 + H 23.19360000 25.05090000 3.73607000 + H 21.98179000 24.55220000 2.94022000 + O 20.19605000 17.40249000 -2.29855000 + H 20.49228000 17.02108000 -1.47153000 + H 19.41714000 17.82667000 -2.04022000 + O 27.00280000 14.85851000 2.33359000 + H 26.27980000 14.80788000 1.70989000 + H 26.82950000 15.78726000 2.62982000 + O 5.75525000 22.13604000 21.73940000 + H 6.13292000 22.59630000 20.91490000 + H 6.48815000 21.79252000 22.22920000 + O 13.42730000 20.04893000 -3.71784000 + H 14.37122000 20.12838000 -3.49284000 + H 13.20209600 19.12589000 -3.39957000 + O 10.41641000 21.23074000 4.64761000 + H 10.08420000 21.82933000 3.96349000 + H 11.39040000 21.30476000 4.52207000 + O 24.67120000 22.65950000 9.10079000 + H 25.08980000 23.37650000 8.67039000 + H 25.13420000 21.80279000 8.86118000 + O 12.17558700 28.26350000 13.25460000 + H 12.42470418 29.04720000 12.75340000 + H 11.99710800 27.64620000 12.52560000 + O 29.67350000 6.20561000 18.95750000 + H 29.68240000 5.23989000 18.92590000 + H 28.86650000 6.45123000 18.52680000 + O 13.99232000 20.97013000 3.48535000 + H 14.20649000 20.77399000 4.39869000 + H 13.33633100 20.21040000 3.31060000 + O 22.15687000 22.95440000 -12.33500000 + H 21.75966000 23.85670000 -12.37350000 + H 23.13770000 23.04350000 -12.51260000 + O 15.61186000 10.71601000 6.69143000 + H 15.06086000 11.50919100 6.70096000 + H 15.71213000 10.42658000 7.64120000 + O 17.43286000 15.83556000 7.01155000 + H 17.67540000 15.72163000 7.95343000 + H 18.24406000 15.89012000 6.52344000 + O 28.63560000 11.55336300 -2.85105000 + H 29.47640000 10.95990000 -2.85949000 + H 28.24330000 11.47423400 -1.95729000 + O 30.81240000 27.87230000 9.37872000 + H 30.12210000 27.92250000 10.02480000 + H 31.45780000 28.59220000 9.59111000 + O 24.85740000 3.70127000 -6.55194000 + H 25.66560000 3.23786000 -6.25474000 + H 24.16430000 2.98413000 -6.45253000 + O 10.29232000 7.61368000 13.63960000 + H 10.45841000 8.49737000 13.27480000 + H 9.27977000 7.49516000 13.58540000 + O 17.89695000 -0.36860000 18.12950000 + H 18.46948000 -0.50690000 18.88220000 + H 17.16944000 -1.01130000 18.27050000 + O 21.48380000 25.94840000 12.92680000 + H 21.49296000 26.84340000 12.64730000 + H 21.87712000 25.91050000 13.82760000 + O 18.96756000 0.46030000 12.30680000 + H 18.32365000 0.93260000 12.84550000 + H 19.88789000 0.75900000 12.52900000 + O 9.16568000 10.22484000 15.18330000 + H 9.20881000 10.03055000 14.23370000 + H 8.23971000 10.41526000 15.49650000 + O 31.58120000 33.06270000 4.17300000 + H 30.67990000 32.84210000 4.00929000 + H 31.59920000 34.01520000 4.03867000 + O 29.04900000 17.17612000 3.61679000 + H 29.83650000 16.71018000 3.87743000 + H 28.32400000 16.85119000 4.10392000 + O -2.04860000 13.68932000 -6.11559000 + H -1.48410000 13.00683400 -6.47353000 + H -2.36000000 13.54280000 -5.19548000 + O 14.84216000 25.33590000 4.10819000 + H 14.36154000 25.93420000 3.57260000 + H 15.21204000 24.76870000 3.45220000 + O 29.72610000 27.05850000 1.43734000 + H 28.81600000 27.17480000 1.71543000 + H 30.10200000 27.09460000 2.27931000 + O 15.84317000 26.29760000 8.37823000 + H 16.65289000 26.39000000 7.90067000 + H 15.94615000 25.56510000 8.97284000 + O 7.50783000 36.03640000 20.12810000 + H 8.39416000 35.89060000 19.75830000 + H 7.58395000 36.56340000 20.97620000 + O 32.72930000 21.17645000 -1.55507000 + H 33.45230000 21.80853000 -1.42815000 + H 32.86040000 20.54820000 -2.30558000 + O 30.05270000 18.63611000 -1.45303000 + H 29.60510000 19.07247000 -2.18930000 + H 30.01600000 19.36078000 -0.77261100 + O 0.56350000 9.32659000 6.38427000 + H 0.50130000 8.42008000 6.73656000 + H 1.45930000 9.59879000 6.56532000 + O 26.76220000 6.58714000 5.75163000 + H 26.62720000 5.87450000 5.13562000 + H 26.47200000 6.34737000 6.61672000 + O 11.32342000 18.62827000 5.58702000 + H 10.37013000 18.36104000 5.55373000 + H 11.73749200 17.83005000 5.89003000 + O 20.55776000 14.42921000 16.81480000 + H 21.18908000 13.89311000 16.26590000 + H 20.96559000 14.80894000 17.63890000 + O 12.47855300 11.82598900 17.45460000 + H 13.38214000 12.10548300 17.27990000 + H 12.50964280 10.90732000 17.79400000 + O 40.09350000 13.02729400 12.06030000 + H 40.76720000 13.18104100 12.69580000 + H 39.61490000 12.26214300 12.45860000 + O 22.31541000 4.46244000 8.17742000 + H 22.04897000 3.62034000 8.58524000 + H 21.70724000 5.05986000 8.69880000 + O 8.00630000 32.60930000 8.05879000 + H 8.65899000 33.23950000 7.70885000 + H 7.92066000 31.92700000 7.38700000 + O 29.41780000 16.05080000 11.63730000 + H 29.73810000 15.72370000 12.51290000 + H 29.50550000 16.99727000 11.68570000 + O 18.90319000 23.34640000 3.40103000 + H 18.05763000 23.41260000 2.85932000 + H 18.74943000 24.08920000 4.05745000 + O 16.91883000 20.20536000 13.14360000 + H 15.99897000 20.34015000 12.89780000 + H 16.82785000 20.00602000 14.13040000 + O 34.80010000 9.83751000 19.81360000 + H 34.92240000 9.72596000 18.86030000 + H 35.69000000 10.01074000 20.13910000 + O 20.02549000 32.53670000 1.52318000 + H 19.82576000 32.86790000 2.44617000 + H 19.31393000 31.96060000 1.16986000 + O 2.61149000 27.60180000 5.82547000 + H 3.43580000 27.91160000 6.23087000 + H 2.88819000 27.03140000 5.10912000 + O 14.31878000 15.81080000 9.95468000 + H 14.95193000 16.06424000 10.67490000 + H 14.92766000 15.16971000 9.48478000 + O 21.96794000 26.37950000 -3.42910000 + H 21.22101000 25.96530000 -2.93926000 + H 22.69920000 26.40280000 -2.79475000 + O 8.49447000 4.67527000 -9.61729000 + H 8.55580000 5.42259000 -8.98787000 + H 8.66183000 3.87274000 -9.04343000 + O 12.86092400 23.13270000 12.05860000 + H 13.25931100 22.32666000 12.42840000 + H 12.77887600 22.99580000 11.11070000 + O 24.54770000 19.12656000 3.12351000 + H 24.28650000 19.02036000 4.03971000 + H 23.77860000 19.40032000 2.60943000 + O 38.90550000 20.87521000 -11.83130000 + H 38.86880000 20.79541000 -10.86690000 + H 38.33510000 20.12284000 -12.22220000 + O 21.18592000 19.03206000 4.83144000 + H 21.89545000 19.75383000 4.67087000 + H 20.39602000 19.53755000 4.74199000 + O 7.79660000 28.89700000 6.19871000 + H 8.55523000 28.76810000 6.84392000 + H 8.09734000 29.77310000 5.96346000 + O 25.82050000 12.60383700 -4.66240000 + H 26.66900000 13.04818200 -4.64394000 + H 25.41810000 12.88925800 -5.47766000 + O 16.76821000 19.72338000 3.29043000 + H 15.95537000 20.04296000 3.70824000 + H 16.68206000 18.74561000 3.39956000 + O 32.01060000 0.64450000 22.11300000 + H 31.79940000 0.76130000 23.07720000 + H 31.66050000 1.49830000 21.74620000 + &END COORD + &END SUBSYS +&END FORCE_EVAL diff --git a/tests/NNP/regtest-1/TEST_FILES.toml b/tests/NNP/regtest-1/TEST_FILES.toml index e5766b8088..e0859b624a 100644 --- a/tests/NNP/regtest-1/TEST_FILES.toml +++ b/tests/NNP/regtest-1/TEST_FILES.toml @@ -8,4 +8,18 @@ "H2O-64_C-NNP_MD-NpT-numeric.inp" = [{matcher="M002", tol=1.0E-14, ref=0.375124841615E+04}] "H2O-64_C-NNP_biased_MD.inp" = [{matcher="M002", tol=1.0E-14, ref=0.375124232248E+04}] "H2O-64_C-NNP_biased_MD_restart.inp" = [{matcher="M002", tol=1.0E-14, ref=0.375124124406E+04}] + +# H2O-256_C-NNP_MD-bitexact: non-cubic 256-water NVT (24.84 x 24.84 +# x 12.42 A, two axes with L > 2*r_cut). Locks the step-zero +# energy, final-step energy and conserved quantity at 1e-14 +# relative. Runtime ~3 s on one rank. The bit-exact conserved +# quantity check may need relaxing in future (Lyapunov-divergent FP +# across compilers), but without it integrator and neighbour-cache +# bugs that let energy drift and blow up the temperature can pass +# silently. +"H2O-256_C-NNP_MD-bitexact.inp" = [ + {matcher="M_INIT_ENERGY", tol=1.0E-14, ref=15004.992479346592518}, + {matcher="M002", tol=1.0E-14, ref=0.150049870566E+05}, + {matcher="M_CONS_QTY", tol=1.0E-14, ref=0.150060861414E+05}, +] #EOF diff --git a/tests/matcher_classes.py b/tests/matcher_classes.py index 086912e865..3ea6e7e9f8 100644 --- a/tests/matcher_classes.py +++ b/tests/matcher_classes.py @@ -25,11 +25,17 @@ class Matcher(Protocol): # ====================================================================================== class GenericMatcher(Matcher): def __init__( - self, pattern: str, col: int, regex: bool = False, abs_value: bool = False + self, + pattern: str, + col: int, + regex: bool = False, + abs_value: bool = False, + first: bool = False, ): self.pattern = pattern self.regex_mode = regex self.abs_value = abs_value + self.first = first if not regex: for c in r"[]()|+*?": pattern = pattern.replace(c, f"\\{c}") @@ -41,7 +47,8 @@ class GenericMatcher(Matcher): assert isinstance(tol, float) or isinstance(ref, int) assert isinstance(ref, float) or isinstance(ref, int) # grep result - for line in reversed(output.split("\n")): + lines = output.split("\n") + for line in lines if self.first else reversed(lines): match = self.regex.search(line) if match: if self.regex_mode and match.groups(): diff --git a/tests/matchers.py b/tests/matchers.py index f91c1f3f69..414601adba 100644 --- a/tests/matchers.py +++ b/tests/matchers.py @@ -307,4 +307,13 @@ registry["E_RIRS_LUMO"] = GenericMatcher(r"G0W0 conduction band minimum", col=6) # Floquet Calculations registry["Quasienergy"] = GenericMatcher(r" 4", col=2) registry["Floquet_DOS"] = GenericMatcher(r"-1.690", col=2) + +# NNP MD matchers. M_INIT_ENERGY passes first=True because ENERGY|Total +# FORCE_EVAL is printed once per MD step, and the default (last-line) +# strategy would return step N rather than the step-0 initial value. +# M_CONS_QTY reads the final-step MD|Conserved quantity. +registry["M_INIT_ENERGY"] = GenericMatcher( + r"ENERGY| Total FORCE_EVAL", col=9, first=True +) +registry["M_CONS_QTY"] = GenericMatcher(r"MD| Conserved quantity", col=5) # EOF From 5a00115c299b734fdeb6a091fab366283e9dcd1f Mon Sep 17 00:00:00 2001 From: Dhruv Date: Wed, 17 Jun 2026 21:25:52 +0100 Subject: [PATCH 6/8] NNP: address code-review feedback (conventions, guards, docs, timing) - nnp_acsf angular OMP loop: convert to DEFAULT(NONE) with explicit SHARED/PRIVATE clauses and replace the two inner ASSOCIATE constructs with direct workspace references (clears coding-conventions c006/c008) - nnp_env_release: guard symfgrp symf/ele/ele_ind deallocations with ALLOCATED; drop the dead radial pack_eta guard (pack_eta is angular-only) - nnp_workspace_grow_caches: note that the cache discard-on-grow is safe - nnp_build_radial_splines, nnp_neighbor_interface_prepare: add timeset/timestop - condense the radial-spline docstring and several inline comment blocks --- src/nnp_acsf.F | 78 +++++++++++++++--------------------- src/nnp_environment_types.F | 16 ++++---- src/nnp_force.F | 4 +- src/nnp_neighbor_interface.F | 8 ++++ 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/src/nnp_acsf.F b/src/nnp_acsf.F index 91834e554d..0d39077719 100644 --- a/src/nnp_acsf.F +++ b/src/nnp_acsf.F @@ -44,13 +44,6 @@ MODULE nnp_acsf LOGICAL, PRIVATE, PARAMETER :: debug_this_module = .FALSE. CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'nnp_acsf' - ! Persistent radial sym-function spline tables, built once on the first - ! nnp_calc_acsf call from the per-(group, sf) eta/rs/cutoff and reused after. - ! Each radial group s holds dense tables spline_y(sf, i)/spline_dy(sf, i) of - ! y(r) = EXP(-eta_sf*(r-rs_sf)^2)*fcut(r) and y'(r). - ! All SFs in a group share the cutoff, so nnp_calc_rad forms the Hermite basis - ! once and streams the sf-first tables. The knot count is set by RAD_SPLINE_N. - ! Cutoff-equality tolerance for grouping symmetry functions in ! nnp_init_acsf_groups: SFs whose cutoffs agree to within this absolute ! tolerance share a group (and a spline grid). @@ -257,7 +250,8 @@ CONTAINS ! Inlined angular kernel: compute + scatter fused. The (j,k) ! geometry is computed once, then the SF loop scatters sym ! values and forces directly into self_dGdr / jj_buf / kk_buf, - ! keeping per-triple scalars in registers. Identical to nnp_calc_ang. + ! keeping per-triple scalars in registers. Inlined equivalent + ! of nnp_calc_ang; the OMP path calls it directly. IF (homo_grp) THEN ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & kk_buf => workspace%dGdr_ang_kk(s)%data, & @@ -743,9 +737,14 @@ CONTAINS ! Each thread allocates its own PRIVATE scratch inside the parallel ! region: an ALLOCATABLE listed as PRIVATE enters unallocated per ! thread, so the ALLOCATE below gives each thread an independent slab. - ! DEFAULT(SHARED) (not NONE) keeps the OPTIONAL `stress` dummy out of - ! the data-sharing clauses; the PRESENT() guard alone gates its use. -!$OMP PARALLEL DEFAULT(SHARED) & + ! Angular groups partition the SF index disjointly, so the shared + ! accumulators (self_dGdr, stress, nnp%ang%y, workspace dGdr) are written + ! race-free. workspace/neighbor are ASSOCIATE names that inherit the + ! data-sharing of their nnp selector; the OPTIONAL stress is SHARED and + ! gated by PRESENT(). +!$OMP PARALLEL DEFAULT(NONE) & +!$OMP SHARED(nnp, ind, self_dGdr, off, stress, & +!$OMP cache_cap_loc, max_ang_symf_loc) & !$OMP PRIVATE(s, j, k, sf, m, l, n_symf_s, n_ang1_s, n_ang2_s, & !$OMP r1, r2, r3, r3_sqr, cutoff_s, cutoff_sqr, homo_grp, & !$OMP rvect1, rvect2, rvect3, & @@ -770,8 +769,6 @@ CONTAINS nnp%cut_type, cutoff_s, fc_c1_loc, dfc_c1_loc) IF (homo_grp) THEN - ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & - kk_buf => workspace%dGdr_ang_kk(s)%data) DO j = 1, n_ang1_s rvect1 = neighbor%ang1(s)%dist(1:3, j) r1 = neighbor%ang1(s)%dist(4, j) @@ -795,12 +792,12 @@ CONTAINS self_dGdr(1, m) = self_dGdr(1, m) + force_loc(1, 1, sf) self_dGdr(2, m) = self_dGdr(2, m) + force_loc(2, 1, sf) self_dGdr(3, m) = self_dGdr(3, m) + force_loc(3, 1, sf) - jj_buf(1, sf, j) = jj_buf(1, sf, j) + force_loc(1, 2, sf) - jj_buf(2, sf, j) = jj_buf(2, sf, j) + force_loc(2, 2, sf) - jj_buf(3, sf, j) = jj_buf(3, sf, j) + force_loc(3, 2, sf) - kk_buf(1, sf, k) = kk_buf(1, sf, k) + force_loc(1, 3, sf) - kk_buf(2, sf, k) = kk_buf(2, sf, k) + force_loc(2, 3, sf) - kk_buf(3, sf, k) = kk_buf(3, sf, k) + force_loc(3, 3, sf) + workspace%dGdr_ang_jj(s)%data(1, sf, j) = workspace%dGdr_ang_jj(s)%data(1, sf, j) + force_loc(1, 2, sf) + workspace%dGdr_ang_jj(s)%data(2, sf, j) = workspace%dGdr_ang_jj(s)%data(2, sf, j) + force_loc(2, 2, sf) + workspace%dGdr_ang_jj(s)%data(3, sf, j) = workspace%dGdr_ang_jj(s)%data(3, sf, j) + force_loc(3, 2, sf) + workspace%dGdr_ang_kk(s)%data(1, sf, k) = workspace%dGdr_ang_kk(s)%data(1, sf, k) + force_loc(1, 3, sf) + workspace%dGdr_ang_kk(s)%data(2, sf, k) = workspace%dGdr_ang_kk(s)%data(2, sf, k) + force_loc(2, 3, sf) + workspace%dGdr_ang_kk(s)%data(3, sf, k) = workspace%dGdr_ang_kk(s)%data(3, sf, k) + force_loc(3, 3, sf) IF (PRESENT(stress)) THEN DO l = 1, 3 stress(:, l, m) = stress(:, l, m) - rvect1(:)*force_loc(l, 2, sf) @@ -812,14 +809,11 @@ CONTAINS END IF END DO END DO - END ASSOCIATE ELSE n_ang2_s = neighbor%n_ang2(s) CALL nnp_fill_fc_dfc_cache(neighbor%ang2(s)%dist, n_ang2_s, & nnp%cut_type, cutoff_s, fc_c2_loc, dfc_c2_loc) - ASSOCIATE (jj_buf => workspace%dGdr_ang_jj(s)%data, & - kk_buf => workspace%dGdr_ang_kk(s)%data) DO j = 1, n_ang1_s rvect1 = neighbor%ang1(s)%dist(1:3, j) r1 = neighbor%ang1(s)%dist(4, j) @@ -843,12 +837,12 @@ CONTAINS self_dGdr(1, m) = self_dGdr(1, m) + force_loc(1, 1, sf) self_dGdr(2, m) = self_dGdr(2, m) + force_loc(2, 1, sf) self_dGdr(3, m) = self_dGdr(3, m) + force_loc(3, 1, sf) - jj_buf(1, sf, j) = jj_buf(1, sf, j) + force_loc(1, 2, sf) - jj_buf(2, sf, j) = jj_buf(2, sf, j) + force_loc(2, 2, sf) - jj_buf(3, sf, j) = jj_buf(3, sf, j) + force_loc(3, 2, sf) - kk_buf(1, sf, k) = kk_buf(1, sf, k) + force_loc(1, 3, sf) - kk_buf(2, sf, k) = kk_buf(2, sf, k) + force_loc(2, 3, sf) - kk_buf(3, sf, k) = kk_buf(3, sf, k) + force_loc(3, 3, sf) + workspace%dGdr_ang_jj(s)%data(1, sf, j) = workspace%dGdr_ang_jj(s)%data(1, sf, j) + force_loc(1, 2, sf) + workspace%dGdr_ang_jj(s)%data(2, sf, j) = workspace%dGdr_ang_jj(s)%data(2, sf, j) + force_loc(2, 2, sf) + workspace%dGdr_ang_jj(s)%data(3, sf, j) = workspace%dGdr_ang_jj(s)%data(3, sf, j) + force_loc(3, 2, sf) + workspace%dGdr_ang_kk(s)%data(1, sf, k) = workspace%dGdr_ang_kk(s)%data(1, sf, k) + force_loc(1, 3, sf) + workspace%dGdr_ang_kk(s)%data(2, sf, k) = workspace%dGdr_ang_kk(s)%data(2, sf, k) + force_loc(2, 3, sf) + workspace%dGdr_ang_kk(s)%data(3, sf, k) = workspace%dGdr_ang_kk(s)%data(3, sf, k) + force_loc(3, 3, sf) IF (PRESENT(stress)) THEN DO l = 1, 3 stress(:, l, m) = stress(:, l, m) - rvect1(:)*force_loc(l, 2, sf) @@ -860,7 +854,6 @@ CONTAINS END IF END DO END DO - END ASSOCIATE END IF END DO !$OMP END DO @@ -1219,27 +1212,23 @@ CONTAINS END SUBROUTINE nnp_calc_rad ! ************************************************************************************************** -!> \brief Build per-radial-group Hermite cubic spline tables. -!> Each radial group s carries its own dense (n_symf, n_grid) tables -!> spline_y and spline_dy. Within a group all SFs share the cutoff -!> => same uniform grid, so the inner SF loop in nnp_calc_rad can -!> compute the Hermite basis params ONCE and stream the per-SF y/dy -!> values contiguously (sf-first layout). -!> -!> Each table stores y(r) = EXP(-eta_sf*(r-rs_sf)^2)*fcut(r) and its -!> analytic derivative y'(r). Memory cost: 2*n_symf*n_grid doubles -!> per group, ~128 KB for n_symf=8, n_grid=8192. Built once per -!> force eval; lives until nnp_env_release. +!> \brief Build sf-first (n_symf, n_grid) Hermite cubic spline tables for radial SFs. +!> Uses a custom sf-first layout (not splines_methods) so nnp_calc_rad can +!> stream contiguous SF values under !$OMP SIMD without indirect addressing. !> \param nnp ... ! ************************************************************************************************** SUBROUTINE nnp_build_radial_splines(nnp) TYPE(nnp_type), INTENT(INOUT), POINTER :: nnp - INTEGER :: ind, k, n_symf, p, s, sf + CHARACTER(len=*), PARAMETER :: routineN = 'nnp_build_radial_splines' + + INTEGER :: handle, ind, k, n_symf, p, s, sf REAL(KIND=dp) :: arg, cutoff, dfcutdr, dr, eta, exp_term, & fcut, r, rs, tanh_tmp + CALL timeset(routineN, handle) + DO ind = 1, nnp%n_ele DO s = 1, nnp%rad(ind)%n_symfgrp ASSOCIATE (grp => nnp%rad(ind)%symfgrp(s)) @@ -1299,6 +1288,8 @@ CONTAINS END DO END DO + CALL timestop(handle) + END SUBROUTINE nnp_build_radial_splines ! ************************************************************************************************** @@ -1428,10 +1419,7 @@ CONTAINS inv_g2 = 0.0_dp END IF - ! ---- Pass 1: angular base + tmpzeta (scalar tight loop) ---- - ! Branchless cusp handling: when tmp <= 0 we set tmpzeta_arr = 0, which - ! propagates to both angular_arr and pref_lam in pass 3, giving - ! sym = 0 and force = 0 without an explicit CYCLE branch. + ! Pass 1: branchless cusp clamp -- zero tmpzeta propagates to sym and pref_lam. DO sf = 1, n_symf tmp = 1.0_dp + grp%pack_lam(sf)*costheta IF (tmp <= 0.0_dp) THEN diff --git a/src/nnp_environment_types.F b/src/nnp_environment_types.F index 77ee32732b..c29e126ae1 100644 --- a/src/nnp_environment_types.F +++ b/src/nnp_environment_types.F @@ -447,11 +447,10 @@ CONTAINS IF (ASSOCIATED(nnp_env%rad)) THEN DO i = 1, nnp_env%n_ele DO j = 1, nnp_env%rad(i)%n_symfgrp - DEALLOCATE (nnp_env%rad(i)%symfgrp(j)%symf, & - nnp_env%rad(i)%symfgrp(j)%ele, & - nnp_env%rad(i)%symfgrp(j)%ele_ind) - IF (ALLOCATED(nnp_env%rad(i)%symfgrp(j)%pack_eta)) & - DEALLOCATE (nnp_env%rad(i)%symfgrp(j)%pack_eta) + IF (ALLOCATED(nnp_env%rad(i)%symfgrp(j)%symf)) & + DEALLOCATE (nnp_env%rad(i)%symfgrp(j)%symf, & + nnp_env%rad(i)%symfgrp(j)%ele, & + nnp_env%rad(i)%symfgrp(j)%ele_ind) IF (ALLOCATED(nnp_env%rad(i)%symfgrp(j)%spline_y)) & DEALLOCATE (nnp_env%rad(i)%symfgrp(j)%spline_y) IF (ALLOCATED(nnp_env%rad(i)%symfgrp(j)%spline_dy)) & @@ -476,9 +475,10 @@ CONTAINS IF (ASSOCIATED(nnp_env%ang)) THEN DO i = 1, nnp_env%n_ele DO j = 1, nnp_env%ang(i)%n_symfgrp - DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%symf, & - nnp_env%ang(i)%symfgrp(j)%ele, & - nnp_env%ang(i)%symfgrp(j)%ele_ind) + IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%symf)) & + DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%symf, & + nnp_env%ang(i)%symfgrp(j)%ele, & + nnp_env%ang(i)%symfgrp(j)%ele_ind) IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%pack_eta)) & DEALLOCATE (nnp_env%ang(i)%symfgrp(j)%pack_eta) IF (ALLOCATED(nnp_env%ang(i)%symfgrp(j)%pack_zeta)) & diff --git a/src/nnp_force.F b/src/nnp_force.F index afcb7c5692..8ed817cbb2 100644 --- a/src/nnp_force.F +++ b/src/nnp_force.F @@ -145,9 +145,7 @@ CONTAINS DO i = 1, nnp%n_ele max_input_nodes = MAX(max_input_nodes, nnp%arc(i)%n_nodes(1)) END DO - ! Per-neighbour dGdr lives in the per-element workspace; the force scatter - ! walks the per-(ind) neighbour lists directly, with no global - ! (3, n_sf, num_atoms) slab. + ! dGdr lives in per-element workspace; no global (3, n_sf, num_atoms) slab. IF (calc_stress) THEN ALLOCATE (stress(3, 3, max_input_nodes)) stress(:, :, :) = 0.0_dp diff --git a/src/nnp_neighbor_interface.F b/src/nnp_neighbor_interface.F index fecbc6b757..a7a6606910 100644 --- a/src/nnp_neighbor_interface.F +++ b/src/nnp_neighbor_interface.F @@ -47,8 +47,13 @@ CONTAINS TYPE(nnp_type), INTENT(INOUT) :: nnp + CHARACTER(len=*), PARAMETER :: routineN = 'nnp_neighbor_interface_prepare' + + INTEGER :: handle LOGICAL :: rebuild + CALL timeset(routineN, handle) + rebuild = .NOT. nnp%neighbor_interface_state%initialized IF (.NOT. rebuild) CALL nnp_neighbor_interface_needs_rebuild(nnp, rebuild) @@ -57,6 +62,8 @@ CONTAINS CALL nnp_neighbor_interface_build_pair_maps(nnp) END IF + CALL timestop(handle) + END SUBROUTINE nnp_neighbor_interface_prepare ! ************************************************************************************************** @@ -442,6 +449,7 @@ CONTAINS IF (workspace%cache_cap >= n_needed) RETURN new_cap = MAX(MAX(8, n_needed), INT(workspace%cache_cap*1.5_dp) + 8) + ! Contents not preserved: nnp_fill_fc_dfc_cache overwrites fully before any read. IF (ALLOCATED(workspace%fc_cache1)) DEALLOCATE (workspace%fc_cache1) IF (ALLOCATED(workspace%dfc_cache1)) DEALLOCATE (workspace%dfc_cache1) IF (ALLOCATED(workspace%fc_cache2)) DEALLOCATE (workspace%fc_cache2) From 80e0d58081938fa0490f8e8f2b8b47485d66f91f Mon Sep 17 00:00:00 2001 From: Dhruv Date: Tue, 7 Jul 2026 16:04:41 +0100 Subject: [PATCH 7/8] NNP: rebin cell-list chain on reuse even when VERLET_SKIN is 0 The Verlet displacement check is guarded by skin > 0, so with VERLET_SKIN 0 a full rebuild never fires. The reuse branch carried the same skin > 0 guard, so nnp_cell_list_rebin_chain was skipped too, leaving the head/next chain stale while atoms moved. Drop the guard so the chain always rebins on an initialised cache; the rebuild branch still wins whenever a rebuild is required. --- src/nnp_cell_list.F | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nnp_cell_list.F b/src/nnp_cell_list.F index 4e1dfbdfc3..05562912e4 100644 --- a/src/nnp_cell_list.F +++ b/src/nnp_cell_list.F @@ -112,8 +112,8 @@ CONTAINS IF (rebuild) THEN CALL nnp_build_cell_list_cache(cache, cell, exact_cutoff, list_cutoff, skin) - ELSE IF (skin > 0.0_dp .AND. cache%initialized) THEN - ! On skin reuse the bin geometry is frozen, but the head/next chain + ELSE IF (cache%initialized) THEN + ! On reuse the bin geometry is frozen, but the head/next chain ! is rebinned from current positions so the walk still finds every ! in-cutoff pair. CALL nnp_cell_list_rebin_chain(cache) From 8be1dfa50f029d924329664bc235169ed89c11dd Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 9 Jul 2026 11:07:32 +0100 Subject: [PATCH 8/8] NNP: address review - unit_nr>0 output guard and default-init workspace neighbor Review feedback (fstein93, PR #5295): - nnp_init_model / nnp_read_committee_weights: fetch the default log unit once unconditionally and gate every write with IF (unit_nr > 0), the canonical CP2K idiom, instead of fetching inside an is_source() branch and reusing it across later branches. - nnp_neighbor_workspace_type: default-initialize the nested neighbor component to nnp_neighbor_type(), with the FTN_NO_DEFAULT_INIT workaround for older compilers (allocatable components via NULL()), mirroring mp2_types.F. --- src/nnp_environment.F | 23 ++++++++++------------- src/nnp_environment_types.F | 11 ++++++++++- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/nnp_environment.F b/src/nnp_environment.F index 0a1100d53f..1da2bd0e97 100644 --- a/src/nnp_environment.F +++ b/src/nnp_environment.F @@ -254,8 +254,8 @@ CONTAINS logger => cp_get_default_logger() - IF (logger%para_env%is_source()) THEN - unit_nr = cp_logger_get_default_unit_nr(logger) + unit_nr = cp_logger_get_default_unit_nr(logger) + IF (unit_nr > 0) THEN WRITE (unit_nr, *) "" WRITE (unit_nr, *) TRIM(printtag)//"| Neural Network Potential Force Environment" END IF @@ -286,8 +286,7 @@ CONTAINS nnp_env%normnodes = .FALSE. nnp_env%n_hlayer = 0 - IF (logger%para_env%is_source()) THEN - unit_nr = cp_logger_get_default_unit_nr(logger) + IF (unit_nr > 0) THEN WRITE (unit_nr, *) TRIM(printtag)//"| Reading NNP input from file: ", TRIM(file_name) END IF @@ -638,7 +637,7 @@ CONTAINS ! read scaling information from file IF (nnp_env%scale_acsf .OR. nnp_env%center_acsf .OR. nnp_env%scale_sigma_acsf) THEN - IF (logger%para_env%is_source()) THEN + IF (unit_nr > 0) THEN WRITE (unit_nr, *) TRIM(printtag)//"| Reading scaling information from file: ", TRIM(file_name) END IF CALL section_vals_val_get(nnp_env%nnp_input, "SCALE_FILE_NAME", & @@ -766,7 +765,7 @@ CONTAINS nnp_env%bias = .FALSE. IF (explicit) THEN IF (nnp_env%n_committee > 1) THEN - IF (logger%para_env%is_source()) THEN + IF (unit_nr > 0) THEN WRITE (unit_nr, *) "NNP| Biasing of committee disagreement enabled" END IF nnp_env%bias = .TRUE. @@ -784,7 +783,7 @@ CONTAINS CPABORT("ALIGN_NNP_ENERGIES size mismatch wrt committee size.") END IF nnp_env%bias_e_avrg(:) = work - IF (logger%para_env%is_source()) THEN + IF (unit_nr > 0) THEN WRITE (unit_nr, *) TRIM(printtag)//"| Biasing is aligned by shifting the energy prediction of the C-NNP members" END IF END IF @@ -793,7 +792,7 @@ CONTAINS END IF END IF - IF (logger%para_env%is_source()) THEN + IF (unit_nr > 0) THEN WRITE (unit_nr, *) TRIM(printtag)//"| NNP force environment initialized" END IF @@ -827,18 +826,16 @@ CONTAINS NULLIFY (logger) logger => cp_get_default_logger() - IF (logger%para_env%is_source()) THEN - unit_nr = cp_logger_get_default_unit_nr(logger) - END IF + unit_nr = cp_logger_get_default_unit_nr(logger) DO i_com = 1, nnp_env%n_committee CALL section_vals_val_get(model_section, "WEIGHTS", c_val=base_name, i_rep_section=i_com) - IF (logger%para_env%is_source()) THEN + IF (unit_nr > 0) THEN WRITE (unit_nr, *) TRIM(printtag)//"| Initializing weights for model: ", i_com END IF DO i = 1, nnp_env%n_ele WRITE (file_name, '(A,I0.3,A)') TRIM(base_name)//".", nnp_env%nuc_ele(i), ".data" - IF (logger%para_env%is_source()) THEN + IF (unit_nr > 0) THEN WRITE (unit_nr, *) TRIM(printtag)//"| Reading weights from file: ", TRIM(file_name) END IF CALL parser_create(parser, file_name, para_env=logger%para_env) diff --git a/src/nnp_environment_types.F b/src/nnp_environment_types.F index c29e126ae1..40cbd0401d 100644 --- a/src/nnp_environment_types.F +++ b/src/nnp_environment_types.F @@ -366,7 +366,16 @@ MODULE nnp_environment_types ! High-water mark of the per-element angular-cache slab, tracked so the 1D ! cutoff caches settle at the true per-element peak. INTEGER :: cache_cap = 0 - TYPE(nnp_neighbor_type) :: neighbor + ! There is a bug with some older compilers preventing requiring an explicit + ! initialization of allocatable components (see mp2_types.F). +#if defined(FTN_NO_DEFAULT_INIT) + TYPE(nnp_neighbor_type) :: neighbor = nnp_neighbor_type( & + pbc_copies=-1, n_rad=NULL(), & + n_ang1=NULL(), n_ang2=NULL(), & + rad=NULL(), ang1=NULL(), ang2=NULL()) +#else + TYPE(nnp_neighbor_type) :: neighbor = nnp_neighbor_type() +#endif REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: radial_sym REAL(KIND=dp), ALLOCATABLE, DIMENSION(:, :) :: radial_force REAL(KIND=dp), ALLOCATABLE, DIMENSION(:) :: angular_sym