diff --git a/AUTHORS.txt b/AUTHORS.txt index 38bd3331b9e9cac3fa2a3e48c8de094e8811d565..338c3351b580ecd411fd8c749f9962b1126efbca 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -10,6 +10,7 @@ # --------------------------------------------------------------- # A more complete author list is provided in the ICON repository. +Daniel Hupp Daniel Reinert Daniel Rieger Florian Prill diff --git a/README.md b/README.md index 09e56b86f28e88c7509d132ca232180d880a6f3d..7baf4ef60422f5f191e233c4201f14225f9ccbf3 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ The `libfortran-support` library includes some general Fortran supporting module - `mo_io_units`: io unit definitions - `mo_namelist`: open/close namelist files - `mo_octree`: octree data structure and operations +- `mo_random_number_generators`: generators for pseudo random numbers (should be moved to math-support once available) - `mo_simple_dump`: array value dumping - `mo_util_backtrace`: function backtrace - `mo_util_file`: file operations diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7db2325be8cb497271dee192f7aaabade11733cc..a1427110ef9a262fc78b3f9c7f516f017b5351c1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,7 @@ add_library(fortran-support mo_io_units.F90 mo_namelist.F90 mo_octree.F90 + mo_random_number_generators.F90 mo_simple_dump.F90 mo_util_backtrace.F90 mo_util_file.F90 diff --git a/src/mo_random_number_generators.F90 b/src/mo_random_number_generators.F90 new file mode 100644 index 0000000000000000000000000000000000000000..e77e04acbde284a696883202b50a4af8d9df76e0 --- /dev/null +++ b/src/mo_random_number_generators.F90 @@ -0,0 +1,152 @@ +! ICON +! +! --------------------------------------------------------------- +! Copyright (C) 2004-2024, DWD, MPI-M, DKRZ, KIT, ETH, MeteoSwiss +! Contact information: icon-model.org +! +! See AUTHORS.TXT for a list of authors +! See LICENSES/ for license information +! SPDX-License-Identifier: BSD-3-Clause +! --------------------------------------------------------------- + +MODULE mo_random_number_generators + + USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: wp => real64 + USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: int32, int64, real64 + + IMPLICIT NONE + PRIVATE + + PUBLIC :: random_normal_values, clip, initialize_random_number_generator, generate_uniform_random_number + + INTERFACE generate_uniform_random_number + MODULE PROCEDURE generate_uniform_random_number_wp + END INTERFACE generate_uniform_random_number + + INTERFACE random_normal_values + MODULE PROCEDURE random_normal_values_wp + END INTERFACE random_normal_values + + INTERFACE clip + MODULE PROCEDURE clip_wp + END INTERFACE clip + + ! integer parameters for using revised minstd parameters as in + ! https://en.wikipedia.org/wiki/Lehmer_random_number_generator#Parameters_in_common_use + INTEGER(KIND=int64), PARAMETER :: minstd_A0 = 16807 + INTEGER(KIND=int64), PARAMETER :: minstd_A = 48271 + + INTEGER(KIND=int64), PARAMETER :: minstd_M = 2147483647 + REAL(KIND=real64), PARAMETER :: minstd_Scale = 1.0_real64/REAL(minstd_M, real64) + +CONTAINS + + ! initializes Lehmer random number generator + PURE FUNCTION initialize_random_number_generator(iseed, jseed) RESULT(minstd_state) + + INTEGER(KIND=int32), INTENT(IN) :: iseed + INTEGER(KIND=int32), INTENT(IN) :: jseed + + INTEGER(KIND=int32) :: minstd_state + + ! doing kind of an xorshift32 see https://en.wikipedia.org/wiki/Xorshift + minstd_state = iseed + minstd_state = IEOR(minstd_state, jseed*2**13) + minstd_state = IEOR(minstd_state, jseed/2**17) + minstd_state = IEOR(minstd_state, jseed*2**5) + + minstd_state = MOD(minstd_A0*minstd_state, minstd_M) + + minstd_state = MOD(minstd_A*minstd_state, minstd_M) + + END FUNCTION initialize_random_number_generator + + ! generated Lehmer random number generator https://en.wikipedia.org/wiki/Lehmer_random_number_generator + FUNCTION generate_uniform_random_number_wp(minstd_state) RESULT(random_number) + + INTEGER(KIND=int32), INTENT(INOUT) :: minstd_state + REAL(KIND=real64) :: random_number + + minstd_state = MOD(minstd_A*minstd_state, minstd_M) + + random_number = ABS(minstd_Scale*minstd_state) + + END FUNCTION generate_uniform_random_number_wp + + SUBROUTINE clip_wp(values_range, values) + + ! Subroutine arguments (in/out/inout) + REAL(wp), INTENT(IN) :: values_range ! allowed range of random numbers + REAL(wp), DIMENSION(:), INTENT(INOUT) :: values ! array of value to be filled with random numbers + + ! Limit random number range + WHERE (values(:) > values_range) + values(:) = values_range + ELSEWHERE(values(:) < -values_range) + values(:) = -values_range + END WHERE + + END SUBROUTINE clip_wp + + SUBROUTINE random_normal_values_wp(seed, values_range, values, mean, stdv) + + ! Subroutine arguments (in/out/inout) + INTEGER, INTENT(IN) :: seed ! seed to be used + REAL(wp), INTENT(IN) :: values_range ! allowed range of random numbers + REAL(wp), DIMENSION(:), INTENT(INOUT) :: values ! array of value to be filled with random numbers + REAL(wp), INTENT(IN), OPTIONAL :: mean + REAL(wp), INTENT(IN), OPTIONAL :: stdv + + REAL(wp), PARAMETER :: pi = 3.141592653589793238_wp ! replace with math constants from math-support once available + + ! Local variables + INTEGER(KIND=int32) :: i, rng_state, n + REAL(KIND=wp) :: u1, u2, z0, z1, magnitude, theta, mu, sigma + + IF (PRESENT(mean)) THEN + mu = mean + ELSE + mu = 0.0_wp + END IF + + IF (PRESENT(stdv)) THEN + sigma = stdv + ELSE + sigma = 1.0_wp + END IF + + n = SIZE(values) + + ! ----------------------------------------------------------------------- + ! Initialize a random number generator, by using the MINSTD linear + ! congruential generator (LCG). "nmaxstreams" indicates that random + ! numbers will be requested in blocks of this length. The generator + ! is seeded with "seed". + !------------------------------------------------------------------------- + + DO i = 1, n/2 + 1 + rng_state = initialize_random_number_generator(seed, i) + + u1 = generate_uniform_random_number(rng_state) + DO WHILE (u1 <= EPSILON(0.0_wp)) + u1 = generate_uniform_random_number(rng_state) + END DO + + u2 = generate_uniform_random_number(rng_state) + + ! using the Box-Muller transform https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform + magnitude = sigma*SQRT(-2.0_wp*LOG(u1)); + theta = 2.0_wp*pi*u2 + + z0 = magnitude*COS(theta) + mu; + z1 = magnitude*SIN(theta) + mu; + values(2*i - 1) = z0 + IF (2*i <= n) values(2*i) = z1 + + END DO + + CALL clip(values_range, values) + + END SUBROUTINE random_normal_values_wp + +END MODULE mo_random_number_generators diff --git a/test/fortran/helpers.f90 b/test/fortran/helpers.f90 index fc2c604e7364f1ba59a5e9f5102eb29b1dd53936..c30ead76b568b328f16ed64f0ce01be35b234adc 100644 --- a/test/fortran/helpers.f90 +++ b/test/fortran/helpers.f90 @@ -13,6 +13,7 @@ MODULE helpers USE ISO_C_BINDING, ONLY: c_double USE mo_io_units, ONLY: find_next_free_unit USE mo_exception, ONLY: message + USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: wp => real64 CONTAINS @@ -52,4 +53,57 @@ CONTAINS OPEN (unit=nerr, file=file, status='old', action='read') END SUBROUTINE open_logfile + + FUNCTION calculate_mean_wp(array) RESULT(mean) + REAL(wp), INTENT(IN) :: array(:) + REAL(wp) :: mean + INTEGER :: i, n + + mean = 0.0_wp + variance = 0.0_wp + + n = SIZE(array) + + DO i = 1, SIZE(array) + mean = mean + array(i) + END DO + mean = mean/n + + END FUNCTION + + FUNCTION calculate_variance_wp(array) RESULT(variance) + REAL(wp), INTENT(IN) :: array(:) + REAL(wp) :: mean, variance + INTEGER :: i, n + + mean = 0.0_wp + variance = 0.0_wp + + n = SIZE(array) + mean = calculate_mean_wp(array) + + DO i = 1, SIZE(array) + variance = variance + (array(i) - mean)**2 + END DO + variance = variance/n + + END FUNCTION + + SUBROUTINE assert_statistics(test_name, array, mean, variance, tol, max, min) + USE FORTUTF + CHARACTER(LEN=*), INTENT(IN) :: test_name + REAL(wp), INTENT(IN) :: array(:) + REAL(wp), INTENT(IN) :: mean, variance, tol, max, min + + CALL TAG_TEST(test_name//"__mean") + CALL ASSERT_ALMOST_EQUAL(calculate_mean_wp(array), mean, tol) + CALL TAG_TEST(test_name//"__variance") + CALL ASSERT_ALMOST_EQUAL(calculate_variance_wp(array), variance, tol) + CALL TAG_TEST(test_name//"__max") + CALL ASSERT_LESS_THAN_EQUAL(MAXVAL(array), max) + CALL TAG_TEST(test_name//"__min") + CALL ASSERT_GREATER_THAN_EQUAL(MINVAL(array), min) + + END SUBROUTINE + END MODULE helpers diff --git a/test/fortran/test_random_number_generators.f90 b/test/fortran/test_random_number_generators.f90 new file mode 100644 index 0000000000000000000000000000000000000000..78f4932561920490df80d85f4390f6ad34c4e636 --- /dev/null +++ b/test/fortran/test_random_number_generators.f90 @@ -0,0 +1,101 @@ +! ICON +! +! --------------------------------------------------------------- +! Copyright (C) 2004-2024, DWD, MPI-M, DKRZ, KIT, ETH, MeteoSwiss +! Contact information: icon-model.org +! +! See AUTHORS.TXT for a list of authors +! See LICENSES/ for license information +! SPDX-License-Identifier: BSD-3-Clause +! --------------------------------------------------------------- + +MODULE TEST_mo_random_number_generators + USE FORTUTF + USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: wp => real64 + USE, INTRINSIC :: ISO_FORTRAN_ENV, ONLY: int32 + USE helpers, ONLY: assert_statistics, calculate_mean_wp, calculate_variance_wp + +CONTAINS + SUBROUTINE TEST_CALCULATE_STATISTICS_WP + REAL(wp) :: array(8) = (/2.0_wp, 4.0_wp, 4.0_wp, 4.0_wp, 5.0_wp, 5.0_wp, 7.0_wp, 9.0_wp/) + CALL TAG_TEST("TEST_calculate_mean_wp") + CALL ASSERT_ALMOST_EQUAL(calculate_mean_wp(array), 5.0_wp) + CALL TAG_TEST("TEST_calculate_variance_wp") + CALL ASSERT_ALMOST_EQUAL(calculate_variance_wp(array), 4.0_wp) + END SUBROUTINE + + SUBROUTINE TEST_CLIP_WP + USE mo_random_number_generators + REAL(wp) :: array(6) = (/4.1_wp, -4.1_wp, 2.0_wp, -2.0_wp, 1.0_wp, -1.0_wp/) + CALL TAG_TEST("TEST_clip_wp") + CALL clip(2.0_wp, array) + CALL ASSERT_EQUAL(array, (/2.0_wp, -2.0_wp, 2.0_wp, -2.0_wp, 1.0_wp, -1.0_wp/)) + END SUBROUTINE + + SUBROUTINE TEST_UNIFORM_RANDOM_NUMBER_WP_BASIC + USE mo_random_number_generators + INTEGER, PARAMETER :: n = 1e5 + INTEGER :: i + INTEGER(int32) :: rng_state + REAL(wp) :: array(n) + + rng_state = initialize_random_number_generator(11, 17) + DO i = 1, n + array(i) = generate_uniform_random_number(rng_state) + END DO + + CALL assert_statistics("TEST_UNIFORM_RANDOM_NUMBER_WP_BASIC", & + array, mean=0.5_wp, variance=(1.0_wp/12.0_wp), tol=1.0e-2_wp, max=1.0_wp, min=0.0_wp) + + END SUBROUTINE + + SUBROUTINE TEST_UNIFORM_RANDOM_NUMBER_WP_ISEED + USE mo_random_number_generators + INTEGER, PARAMETER :: n = 1e5 + INTEGER :: i + INTEGER(int32) :: rng_state + REAL(wp) :: array(n) + + DO i = 1, n + rng_state = initialize_random_number_generator(i, 11) + array(i) = generate_uniform_random_number(rng_state) + END DO + + CALL assert_statistics("TEST_UNIFORM_RANDOM_NUMBER_WP_ISEED", & + array, mean=0.5_wp, variance=(1.0_wp/12.0_wp), tol=1.0e-2_wp, max=1.0_wp, min=0.0_wp) + + END SUBROUTINE + + SUBROUTINE TEST_UNIFORM_RANDOM_NUMBER_WP_JSEED + USE mo_random_number_generators + INTEGER, PARAMETER :: n = 1e5 + INTEGER :: i + INTEGER(int32) :: rng_state + REAL(wp) :: array(n) + + DO i = 1, n + rng_state = initialize_random_number_generator(11, i) + array(i) = generate_uniform_random_number(rng_state) + END DO + + CALL assert_statistics("TEST_UNIFORM_RANDOM_NUMBER_WP_JSEED", & + array, mean=0.5_wp, variance=(1.0_wp/12.0_wp), tol=1.0e-2_wp, max=1.0_wp, min=0.0_wp) + + END SUBROUTINE + + SUBROUTINE TEST_RANDOM_NORMAL_VALUES_WP + USE mo_random_number_generators + INTEGER, PARAMETER :: n = 1e5 + 1 + REAL(wp) :: array(n) + REAL(wp) :: mean = -5.0_wp + REAL(wp) :: variance = 7.1_wp + REAL(wp) :: value_range = 1.0e32_wp + + CALL random_normal_values(seed=11, values_range=value_range, values=array, mean=mean, stdv=SQRT(variance)) + + CALL assert_statistics("TEST_RANDOM_NORMAL_VALUES_WP", & + array, mean=mean, variance=variance, tol=1.0e-2_wp, max=value_range, min=-value_range) + + END SUBROUTINE + +END MODULE TEST_mo_random_number_generators