!>
!> @file buffer.f90
!>
!> @brief Storage/Buffer for 0D diagnostic quantities.
!>
!> @copyright
!> Copyright (©) 2021 EPFL (Ecole Polytechnique Fédérale de Lausanne)
!> SPC (Swiss Plasma Center)
!>
!> futils is free software: you can redistribute it and/or modify it under
!> the terms of the GNU Lesser General Public License as published by the Free
!> Software Foundation, either version 3 of the License, or (at your option)
!> any later version.
!>
!> futils is distributed in the hope that it will be useful, but WITHOUT ANY
!> WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
!> FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
!>
!> You should have received a copy of the GNU Lesser General Public License
!> along with this program. If not, see .
!>
!> @authors
!> (in alphabetical order)
!> @author Claudio Gheller
!> @author Ben McMillan
!> @author Noé Ohana
!> @author Trach-Minh Tran
!>
! Uses a hashtable to store the data. The key is a string
! containing the name of the diagnostic.
MODULE hashtable
USE hdf5
USE futils
USE mpi
IMPLICIT NONE
PRIVATE
PUBLIC :: ref_htable, add_record, htable_to_hdf5_int, htable_init, htable_endstep
PUBLIC :: htable_hdf5_flush,set_htable_fileid
INTEGER, PARAMETER :: maxnamelen=32
INTEGER, PARAMETER :: maxdesclen=256
TYPE, PUBLIC :: mpi_reduce_pars
INTEGER :: communicator
INTEGER :: mpiop
LOGICAL :: do_nothing
END TYPE mpi_reduce_pars
! Datatype for storing a 0D diagnostic in a list.
TYPE, PUBLIC :: lel
! Name of the diagnostic (HDF5 datagroup).
INTEGER :: name_length
CHARACTER(len=maxnamelen) :: name
! Description of the diagnostic.
INTEGER :: description_length
CHARACTER(len=maxdesclen) :: description
! Buffer of values.
DOUBLE PRECISION, POINTER, DIMENSION(:) :: value => NULL()
! Next element in list
TYPE(lel), POINTER :: next => NULL()
! Collective operation MPI
TYPE(mpi_reduce_pars) :: mpiparams
INTEGER :: mpiparamno
END type lel
TYPE, PUBLIC :: lelptr
TYPE(lel), POINTER :: pntr => NULL()
END TYPE lelptr
! Iterator for the hashtable
TYPE leliter
INTEGER :: num ! Position in Hashtable array
TYPE(lelptr), DIMENSION(:), POINTER :: root ! Iterator's Hashtable
TYPE(lel), POINTER :: ptr ! Current element in chained list.
END TYPE leliter
TYPE, PUBLIC :: BUFFER_TYPE
! HDF5 file handle
INTEGER :: fresid
! Name of group to place 0D variables in.
CHARACTER(len=maxdesclen) :: groupname
! node which performs IO via futils/HDF5
INTEGER :: ionode
! Communicators for parallel sum
TYPE(mpi_reduce_pars), DIMENSION(:), POINTER :: mpireducepars => null()
INTEGER, DIMENSION(:), POINTER :: mpireducenum => null()
INTEGER, DIMENSION(:), POINTER :: mpireduceindex => null()
INTEGER :: nmpipars
! Length of buffer
INTEGER :: buffer_length = 1
INTEGER :: current_buffer_pos = 1
! Mask for mapping the hash value into the array
INTEGER :: tablelength_mask
! Number of items in hashtable
INTEGER :: numels = 0
! The hashtable: an array of lists
TYPE(lelptr), POINTER, DIMENSION(:) :: htable => null()
! Temporary arrays for MPI sum
DOUBLE PRECISION, DIMENSION(:,:), POINTER :: psum_arr => null()
DOUBLE PRECISION, DIMENSION(:,:), POINTER :: psum_arr2 => null()
END TYPE BUFFER_TYPE
! We want the pointer members of TYPE(lel) to behave differently under
! assignment: the 'value' element should be copied across into
! freshly allocated storage.
INTERFACE ASSIGNMENT (=)
MODULE PROCEDURE ASSIGN_LEL_TO_LEL
END INTERFACE ASSIGNMENT (=)
INTERFACE OPERATOR (==)
MODULE PROCEDURE EQUALITY_TYPE_MPARAMS
END INTERFACE OPERATOR (==)
CONTAINS
SUBROUTINE ASSIGN_LEL_TO_LEL(el1,el2)
TYPE(lel), INTENT(OUT) :: el1
TYPE(lel), INTENT(IN) :: el2
el1%name = el2%name
el1%mpiparamno = el2%mpiparamno
el1%mpiparams = el2%mpiparams
el1%name_length = el2%name_length
el1%description = el2%description
el1%description_length = el2%description_length
IF(ASSOCIATED(el1%value)) THEN
DEALLOCATE(el1%value)
END IF
ALLOCATE(el1%value(SIZE(el2%value)))
el1%value = el2%value
END SUBROUTINE ASSIGN_LEL_TO_LEL
FUNCTION EQUALITY_TYPE_MPARAMS(mp1,mp2)
TYPE(mpi_reduce_pars), INTENT(IN) :: mp1,mp2
LOGICAL :: EQUALITY_TYPE_MPARAMS
EQUALITY_TYPE_MPARAMS= &
& ((mp1%communicator==mp2%communicator).AND.(mp1%mpiop==mp2%mpiop)).OR.(mp1%do_nothing.AND.mp2%do_nothing)
END FUNCTION EQUALITY_TYPE_MPARAMS
SUBROUTINE set_htable_fileid(buf,fresid_in,groupname_in)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
INTEGER, INTENT(IN) :: fresid_in
CHARACTER(len=*), INTENT(IN), OPTIONAL :: groupname_in
CHARACTER(len=*), parameter :: default_groupname = '/data/var0d/'
buf%fresid = fresid_in
IF(PRESENT(groupname_in)) THEN
buf%groupname = groupname_in
ELSE
buf%groupname = default_groupname
END IF
END SUBROUTINE SET_HTABLE_FILEID
SUBROUTINE htable_init(buf,buffer_length_in,ionode_in)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
INTEGER, INTENT(IN), OPTIONAL :: buffer_length_in
INTEGER, INTENT(IN), OPTIONAL :: ionode_in
! Default ionode is zero.
IF(PRESENT(ionode_in)) THEN
buf%ionode = ionode_in
ELSE
buf%ionode = 0
END IF
IF(PRESENT(buffer_length_in)) THEN
buf%buffer_length = buffer_length_in
ELSE
buf%buffer_length = 1
END IF
END SUBROUTINE htable_init
! Add element to list (interface to recursive function `add')
SUBROUTINE add_element(el,lptr)
TYPE(lel), POINTER :: el
TYPE(lelptr) :: lptr
lptr%pntr => add(el,lptr%pntr)
END SUBROUTINE add_element
RECURSIVE FUNCTION add(el,list) RESULT(add_res)
TYPE(lel), POINTER :: list,el,add_res
IF (.not.associated(list)) THEN
el%next => list
add_res => el
RETURN
END IF
IF (list%name list
add_res => el
RETURN
END IF
list%next => add(el,list%next)
add_res => list
END FUNCTION add
! Resize hashtable to power of 2 >= length_hint.
SUBROUTINE set_htable_size(buf,length_hint)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
INTEGER, INTENT(IN) :: length_hint
INTEGER :: log2size,htable_size,one
TYPE(lelptr), DIMENSION(:), POINTER :: old_htable
TYPE(lel), POINTER :: curr,new_el
TYPE(leliter) :: iter
one = 1
log2size = CEILING(LOG(1.0*length_hint)/LOG(2.0))
htable_size = 2**log2size
buf%tablelength_mask = ISHFTC(one,log2size)-1
IF(ASSOCIATED(buf%htable)) THEN
! Copy elements into new hashtable.
old_htable => buf%htable
ALLOCATE(buf%htable(0:htable_size-1))
CALL restart(old_htable,iter)
DO WHILE(.NOT.finished(iter))
curr => iter%ptr
ALLOCATE(new_el)
new_el = curr
CALL add_element(new_el,buf%htable(hash(buf,curr%name)))
CALL increment(iter)
IF(ASSOCIATED(curr)) THEN
DEALLOCATE(curr)
END IF
END DO
DEALLOCATE(old_htable)
ELSE
ALLOCATE(buf%htable(0:htable_size-1))
END IF
END SUBROUTINE SET_HTABLE_SIZE
! Place iterator at first element in
! hashtable
SUBROUTINE restart(hashtable,iter)
TYPE(lelptr), POINTER, DIMENSION(:) :: hashtable
TYPE(leliter) :: iter
iter%num=0
iter%root=>hashtable
iter%ptr=>hashtable(iter%num)%pntr
IF(.NOT.ASSOCIATED(iter%ptr)) THEN
CALL increment(iter)
END IF
END SUBROUTINE restart
! Move to next entry in hash table
SUBROUTINE increment(iter)
TYPE(leliter) :: iter
! Move along the list.
IF (ASSOCIATED(iter%ptr)) THEN
iter%ptr => iter%ptr%next
END IF
! We have fallen off the chain.
IF(.NOT.ASSOCIATED(iter%ptr)) THEN
! End of the chained list.
! Find next nonempty list and position iterator at start
DO
iter%num =iter%num+1
IF(iter%num>=size(iter%root)) EXIT
IF(ASSOCIATED(iter%root(iter%num)%pntr)) EXIT
END DO
IF( iter%num < size(iter%root) ) THEN
iter%ptr => iter%root(iter%num)%pntr
ELSE
iter%ptr => null()
END IF
END IF
END SUBROUTINE increment
! Check whether iterator is finished
LOGICAL FUNCTION finished(iter)
TYPE(leliter) :: iter
finished = (iter%num.GE.size(iter%root))
END FUNCTION finished
INTEGER FUNCTION hash(buf,in)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
CHARACTER(len=*) :: in
hash = hash_int(in,buf%tablelength_mask)
END FUNCTION hash
! hash function based on
! Jenkin's one at a time hash
INTEGER FUNCTION hash_int(in,lengthmask)
CHARACTER(len=*), INTENT(IN) :: in
INTEGER, INTENT(IN) :: lengthmask
INTEGER :: i, ch
hash_int = 0
DO i=1,len(in)
ch = ICHAR(in(i:i))
hash_int = hash_int + ch
hash_int = hash_int + ISHFT(hash_int, 10)
hash_int = IEOR(hash_int, ISHFT(hash_int,-6))
END DO
hash_int = hash_int + ISHFT(hash_int,3)
hash_int = IEOR(hash_int,ISHFT(hash_int,-11))
hash_int = hash_int + ISHFT(hash_int,15)
hash_int = IAND(hash_int,lengthmask)
END FUNCTION hash_int
! Add value to hashtable, creating new record if necessary
SUBROUTINE add_record(buf,name,description,value,parallel_comm, mpi_operation)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
CHARACTER(len=*), INTENT(IN) :: name
CHARACTER(len=*), INTENT(IN) :: description
DOUBLE PRECISION, INTENT(IN) :: value
INTEGER, OPTIONAL,INTENT(IN) :: parallel_comm
INTEGER, OPTIONAL,INTENT(IN) :: mpi_operation
TYPE(lel), POINTER :: record
INTEGER :: pcomm, mpiop
record => ref_htable(buf,name)
IF (.NOT.ASSOCIATED(record)) THEN
! Not already in table
CALL add_to_htable(buf,name,description,parallel_comm,mpi_operation)
record => ref_htable(buf,name)
END IF
! May be safer to explicitly allocate memory
! for the buffer here rather than elsewhere
IF (.NOT.ASSOCIATED(record%value)) THEN
ALLOCATE(record%value(buf%buffer_length))
record%value = 0.0
END IF
record%value(buf%current_buffer_pos) = value
END SUBROUTINE add_record
SUBROUTINE add_to_htable(buf,name,description,pcomm, mpiop)
TYPE(BUFFER_TYPE) :: buf
CHARACTER(len=*), INTENT(IN) :: name,description
INTEGER, INTENT(IN), OPTIONAL :: pcomm
INTEGER, INTENT(IN), OPTIONAL :: mpiop
TYPE(lel), POINTER :: newel
LOGICAL :: resize
buf%numels = buf%numels + 1
! resize hashtable if necessary.
IF(.NOT.ASSOCIATED(buf%htable)) THEN
resize=.true.
ELSE
resize=(buf%numels>SIZE(buf%htable))
END IF
IF (resize) CALL set_htable_size(buf,buf%numels)
ALLOCATE(newel)
IF(PRESENT(pcomm)) newel%mpiparams%communicator = pcomm
IF(PRESENT(mpiop)) THEN
newel%mpiparams%mpiop = mpiop
ELSE
newel%mpiparams%mpiop = MPI_SUM
END IF
newel%mpiparams%do_nothing = .NOT.PRESENT(pcomm)
newel%name = name
newel%name_length = min(len(newel%name),len(name))
newel%description = description
newel%description_length = min(len(newel%description),len(description))
CALL add_element(newel,buf%htable(hash(buf,newel%name)))
END SUBROUTINE add_to_htable
! Print out the hashes, and the elements
SUBROUTINE print_htable(buf)
TYPE(BUFFER_TYPE) :: buf
INTEGER :: i
TYPE(lel), POINTER :: curr
DO i=0,size(buf%htable)-1
print *,'Hash: ',i
curr => buf%htable(i)%pntr
DO WHILE(ASSOCIATED(curr))
print *,'el: "',curr%name, '"'
curr => curr%next
END DO
END DO
END SUBROUTINE print_htable
! After all the data has been placed in the buffer,
! advance along the buffer and flush if necessary
SUBROUTINE htable_endstep(buf)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
buf%current_buffer_pos = buf%current_buffer_pos + 1
IF (buf%current_buffer_pos > buf%buffer_length) THEN
buf%current_buffer_pos = 1
CALL htable_to_hdf5_int(buf,buf%buffer_length)
END IF
END SUBROUTINE htable_endstep
! Clear buffer without flushing
!
SUBROUTINE htable_clear(buf)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
buf%current_buffer_pos = 1
END SUBROUTINE htable_clear
! Flush previously written data
! after a time step
SUBROUTINE htable_hdf5_flush(buf)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
CALL htable_to_hdf5_int(buf,buf%current_buffer_pos - 1)
buf%current_buffer_pos = 1
END SUBROUTINE htable_hdf5_flush
! Output the contents to the HDF5 file, creating
! descriptors if necessary
SUBROUTINE htable_to_hdf5_int(buf,num_elements)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
TYPE(leliter) :: iter
TYPE(lel), POINTER :: curr
INTEGER, DIMENSION(1) :: dims = (/0/)
INTEGER, INTENT(IN) :: num_elements
INTEGER :: num, me_world, ierr
IF(num_elements==0) RETURN
! Do parallel sum
CALL psum_htable(buf)
CALL restart(buf%htable,iter)
CALL MPI_COMM_RANK(MPI_COMM_WORLD,me_world,ierr)
IF(me_world/=buf%ionode) RETURN
num = 1
DO WHILE(.NOT.finished(iter))
curr => iter%ptr
IF (.NOT.isdataset(buf%fresid, TRIM(buf%groupname)//'/'//curr%name(1:curr%name_length))) THEN
!PRINT *,'Create dataset: ',TRIM(groupname)//'/'//curr%name(1:curr%name_length),'::', &
! &curr%description(1:curr%description_length)
!PRINT *,'fresid: ',fresid
CALL creatd(buf%fresid, 0, dims, TRIM(buf%groupname)//'/'//curr%name(1:curr%name_length), &
& curr%description(1:curr%description_length))
CALL attach(buf%fresid,TRIM(buf%groupname)//'/'//curr%name(1:curr%name_length), &
& 'PlotOrder',num)
END IF
!PRINT *,'Append value: ', curr%name(1:curr%name_length), num_elements, curr%value(1:num_elements)
CALL append(buf%fresid,TRIM(buf%groupname)//'/'//curr%name(1:curr%name_length),curr%value(1:num_elements))
!PRINT *,'~Append value: '
CALL increment(iter)
num = num + 1
END DO
END SUBROUTINE htable_to_hdf5_int
FUNCTION ref_htable(buf,name)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
CHARACTER(len=*) :: name
TYPE(lel), POINTER :: ref_htable
TYPE(lel), POINTER :: curr
TYPE(lel) :: del
IF(.NOT.ASSOCIATED(buf%htable)) THEN
ref_htable => null()
RETURN
END IF
del%name = name
curr => buf%htable(hash(buf,del%name))%pntr
DO WHILE(ASSOCIATED(curr))
IF (curr%name==name) THEN
ref_htable => curr
RETURN
END IF
curr => curr%next
END DO
ref_htable => null()
END FUNCTION ref_htable
! Do a parallel sum over some values in the hashtable,
! using an array to consolidate it into a single MPI operation.
SUBROUTINE psum_htable(buf)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
CALL find_reduce_pars(buf)
CALL psum_htable_int(buf)
END SUBROUTINE psum_htable
! Find all the different combinations of (MPI_COMMUNICATOR,MPI_OPERATION)
! and put them in a list.
! This could be optimised by using a sorting algorithm, but probably
! not useful except for very large cases.
SUBROUTINE find_reduce_pars(buf)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
TYPE(lel), POINTER :: curr
TYPE(leliter) :: iter
INTEGER :: paramno, i, cnum
IF(.NOT.ASSOCIATED(buf%mpireducenum)) THEN
ALLOCATE(buf%mpireducepars(buf%numels))
ALLOCATE(buf%mpireduceindex(buf%numels))
ALLOCATE(buf%mpireducenum(buf%numels))
END IF
IF(SIZE(buf%mpireducepars) iter%ptr
paramno = 0
DO i=1, buf%nmpipars
if (buf%mpireducepars(i) == curr%mpiparams) THEN
paramno = i
END IF
END DO
IF (paramno==0) THEN
buf%nmpipars = buf%nmpipars +1
buf%mpireducepars(buf%nmpipars) = curr%mpiparams
paramno = buf%nmpipars
END IF
curr % mpiparamno = paramno
buf%mpireducenum(paramno) = buf%mpireducenum(paramno) + 1
CALL increment(iter)
END DO
cnum = 1
buf%mpireduceindex(1) = cnum
DO i=2,buf%nmpipars
buf%mpireduceindex(i) = buf%mpireducenum(i-1) + cnum
cnum = buf%mpireduceindex(i)
END DO
END SUBROUTINE find_reduce_pars
! For each communicator group.
SUBROUTINE psum_htable_int(buf)
TYPE(BUFFER_TYPE), INTENT(INOUT) :: buf
INTEGER :: ierr,curr_num
TYPE(leliter) :: iter
TYPE(lel), POINTER :: curr
INTEGER :: me_world,num_send,group,mpiop
INTEGER :: i
IF(ASSOCIATED(buf%psum_arr)) THEN
IF(SIZE(buf%psum_arr,2) iter%ptr
buf%psum_arr(:, buf%mpireducenum(curr%mpiparamno)+buf%mpireduceindex(curr%mpiparamno) ) = curr%value
buf%mpireducenum(curr%mpiparamno) = buf%mpireducenum(curr%mpiparamno) + 1
CALL increment(iter)
END DO
num_send = SIZE(buf%psum_arr,1)*buf%numels
DO i=1, buf%nmpipars
group = buf%mpireducepars(i)%communicator
mpiop = buf%mpireducepars(i)%mpiop
IF(.NOT.buf%mpireducepars(i)%do_nothing) THEN
CALL MPI_ALLREDUCE(buf%psum_arr( 1,buf%mpireduceindex(i)), &
& buf%psum_arr2(1,buf%mpireduceindex(i)), &
& buf%mpireducenum(i)*buf%buffer_length, &
& MPI_DOUBLE_PRECISION, mpiop, group, ierr)
ELSE
buf%psum_arr2( :,buf%mpireduceindex(i):buf%mpireduceindex(i)+buf%mpireducenum(i)-1) &
& = buf%psum_arr( :,buf%mpireduceindex(i):buf%mpireduceindex(i)+buf%mpireducenum(i)-1)
END IF
END DO
! Put values back into hashtable
buf%mpireducenum(:) = 0
CALL restart(buf%htable,iter)
DO WHILE(.NOT.finished(iter))
curr => iter%ptr
curr%value = buf%psum_arr2(:, buf%mpireducenum(curr%mpiparamno)+buf%mpireduceindex(curr%mpiparamno) )
buf%mpireducenum(curr%mpiparamno) = buf%mpireducenum(curr%mpiparamno) + 1
CALL increment(iter)
END DO
END SUBROUTINE psum_htable_int
END MODULE hashtable