From 7c198c9b32357c313fd8aba3e702f90b4c52f9bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 20:47:03 +0000 Subject: [PATCH 1/2] pre-commit: Use stable ruff rules instead of preview Drop --preview from ruff in pre-commit and the Python code quality workflow, and remove the preview-only pydoclint (DOC) selection and ignores, which have no effect without preview. --- .github/workflows/python-code-quality.yml | 4 ++-- .pre-commit-config.yaml | 2 +- pyproject.toml | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index 74c420d69e8..2b271e7a094 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -70,10 +70,10 @@ jobs: - name: Install Ruff run: pip install --break-system-packages "ruff==${RUFF_VERSION}" - name: Run Ruff (output annotations on fixable errors) - run: ruff check --output-format=github . --preview --unsafe-fixes + run: ruff check --output-format=github . --unsafe-fixes continue-on-error: true - name: Run Ruff (apply fixes for suggestions) - run: ruff check . --preview --fix --unsafe-fixes + run: ruff check . --fix --unsafe-fixes - name: Run `ruff format` showing diff without failing continue-on-error: true if: ${{ !cancelled() }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c7c65b29dd..52aae79338a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: hooks: # Run the linter. - id: ruff-check - args: [--fix, --preview] + args: [--fix] # Run the formatter. - id: ruff-format - repo: https://github.com/igorshubovych/markdownlint-cli diff --git a/pyproject.toml b/pyproject.toml index 71be308a430..6ea5dfa580c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,6 @@ select = [ "D209", # pydocstyle (D) new-line-after-last-paragraph "D211", # pydocstyle (D) blank-line-before-class "D212", # pydocstyle (D) multi-line-summary-first-line - "DOC", # pydoclint (DOC) "DTZ", # flake8-datetimez (DTZ) "E", # pycodestyle (E, W) "EM", # flake8-errmsg (EM) @@ -223,9 +222,6 @@ ignore = [ "D415", # missing-terminal-punctuation "D416", # missing-section-name-colon "D419", # empty-docstring - "DOC201", # docstring-missing-returns - "DOC402", # docstring-missing-yields - "DOC501", # docstring-missing-exception "DTZ001", # call-datetime-without-tzinfo "DTZ002", # call-datetime-today "DTZ005", # call-datetime-now-without-tzinfo From 14b078e8240211e143b444ebb2e2dfe77d269d9c Mon Sep 17 00:00:00 2001 From: Vaclav Petras Date: Wed, 28 Oct 2026 22:07:10 -0400 Subject: [PATCH 2/2] r.mapcalc: Enable parallel rand() with reproducible results (#8114) rand() drew from the generator shared by the whole program, which hands out values to whichever thread asks first, so r.mapcalc evaluated expressions with rand() on one thread only, whatever nprocs was. Every rand() call draws one value per cell of a row, and every row evaluates every call once, so a row draws columns times calls values. r.mapcalc now builds an exact random number layout from the seed with a row as its unit (in r3.mapcalc, the rows of all depths one after another), and each row places a generator state of its own at its stream before it is evaluated. The state is local to the evaluation of the row, so the draws of different threads do not write to the same cache line. The streams of an exact layout are the parts of the sequence which the rows drew when they were evaluated one after another from the shared generator, so the result for a seed stays the same as in earlier versions and is the same for any nprocs, and rand() no longer turns off parallel evaluation. A warning is given when the whole computation draws more than 2^46 values, after which the values repeat plus a constant. The seed is read with G_random_seed_from_option(), so a seed outside -2147483648 to 4294967295 is now an error rather than being silently reduced to its low 32 bits, and a seed with trailing characters is an error rather than being cut off. The automatic seed comes from G_random_generate_seed(), so it can be set with GRASS_RANDOM_SEED or SOURCE_DATE_EPOCH, and is still written to the history. rand() stays in the calc library. The library gains calc_set_random_state(), through which a tool gives rand() a function returning the calling thread's generator state, and r.mapcalc sets it to the state it places for the row. When no function is set, rand() draws from the generator shared by the whole program as before, so other users of the library get the same values as before. --- include/grass/defs/calc.h | 2 + lib/calc/xrand.c | 46 ++++- raster/r.mapcalc/evaluate.c | 46 +++++ raster/r.mapcalc/globals.h | 5 +- raster/r.mapcalc/main.c | 61 +++---- raster/r.mapcalc/map.c | 3 +- raster/r.mapcalc/r.mapcalc.html | 27 ++- raster/r.mapcalc/r.mapcalc.md | 4 +- raster/r.mapcalc/r3.mapcalc.html | 25 ++- raster/r.mapcalc/r3.mapcalc.md | 8 +- .../r.mapcalc/tests/r_mapcalc_nprocs_test.py | 10 - .../tests/r_mapcalc_rand_layout_test.py | 172 ++++++++++++++++++ .../tests/r_mapcalc_rand_seed_test.py | 19 +- 13 files changed, 332 insertions(+), 96 deletions(-) create mode 100644 raster/r.mapcalc/tests/r_mapcalc_rand_layout_test.py diff --git a/include/grass/defs/calc.h b/include/grass/defs/calc.h index 706974f7b39..cd28982bf9d 100644 --- a/include/grass/defs/calc.h +++ b/include/grass/defs/calc.h @@ -3,6 +3,8 @@ extern void calc_init(int); +extern void calc_set_random_state(struct G_random_state *(*)(void)); + extern func_t f_add; extern func_t f_sub; extern func_t f_mul; diff --git a/lib/calc/xrand.c b/lib/calc/xrand.c index a36c5645e8d..90af0b4ef1a 100644 --- a/lib/calc/xrand.c +++ b/lib/calc/xrand.c @@ -9,8 +9,48 @@ rand(lo,hi) random values between a and b ****************************************************************/ +/* The function set by calc_set_random_state(), or NULL. */ +static struct G_random_state *(*random_state)(void); + +/*! + * \brief Set the function which gives rand() the random number state + * + * The function must return the state of the calling thread, which the + * tool places for the row being evaluated. Without a function, that is, + * with NULL, rand() draws from the generator shared by the whole program. + * + * \param get_state function returning the calling thread's state, or NULL + */ +void calc_set_random_state(struct G_random_state *(*get_state)(void)) +{ + random_state = get_state; +} + +/* Draw from the state, or from the shared generator without one. */ +static unsigned int draw_uint(struct G_random_state *state) +{ + if (state) + return G_random_uint32(state); + return (unsigned int)G_mrand48(); +} + +/* Draw from the state, or from the shared generator without one. */ +static double draw_double(struct G_random_state *state) +{ + if (state) + return G_random_double(state); + return G_drand48(); +} + +/*! + * \brief Random value between two values, the upper one excluded + * + * Draws one value per column from the state given by the function set + * with calc_set_random_state(), or from the shared generator. + */ int f_rand(int argc, const int *argt, void **args) { + struct G_random_state *state = random_state ? random_state() : NULL; int i; if (argc < 2) @@ -25,7 +65,7 @@ int f_rand(int argc, const int *argt, void **args) CELL *arg2 = args[2]; for (i = 0; i < columns; i++) { - unsigned int x = (unsigned int)G_mrand48(); + unsigned int x = draw_uint(state); int lo = arg1[i]; int hi = arg2[i]; @@ -45,7 +85,7 @@ int f_rand(int argc, const int *argt, void **args) FCELL *arg2 = args[2]; for (i = 0; i < columns; i++) { - double x = G_drand48(); + double x = draw_double(state); FCELL lo = arg1[i]; FCELL hi = arg2[i]; @@ -65,7 +105,7 @@ int f_rand(int argc, const int *argt, void **args) DCELL *arg2 = args[2]; for (i = 0; i < columns; i++) { - double x = G_drand48(); + double x = draw_double(state); DCELL lo = arg1[i]; DCELL hi = arg2[i]; diff --git a/raster/r.mapcalc/evaluate.c b/raster/r.mapcalc/evaluate.c index b93a412d29d..f01bbb73a38 100644 --- a/raster/r.mapcalc/evaluate.c +++ b/raster/r.mapcalc/evaluate.c @@ -2,6 +2,7 @@ #include #endif +#include #include #include #include @@ -357,6 +358,36 @@ static expr_list *exprs; /****************************************************************************/ +/* The layout rand() draws from, with a row as its unit. Every rand() call + * draws one value per cell, so every row draws columns times calls values. + * A row of r3.mapcalc is numbered across the depths. */ +static struct G_random_layout rand_layout; +/* The state of the row each thread evaluates. The states themselves are + * local to the row's evaluation, so the draws of different threads do not + * write to the same cache line. */ +static struct G_random_state **rand_state; + +/* Give rand() of the calc library the calling thread's state. */ +static struct G_random_state *thread_rand_state(void) +{ + int tid = 0; +#if defined(_OPENMP) + tid = omp_get_thread_num(); +#endif + return rand_state[tid]; +} + +/* Build the layout for the region and the number of rand() calls. */ +static void setup_rand(void) +{ + G_random_init_layout_exact(&rand_layout, seed_value, (int64_t)depths * rows, + (int64_t)columns * rand_calls); + if (G_random_layout_batches(&rand_layout) < 1) + G_warning(_("rand() draws more than 2^46 random values. Later " + "rows repeat the values of earlier rows plus a " + "constant.")); +} + static void error_handler(void *p G_UNUSED) { expr_list *l; @@ -421,6 +452,8 @@ void execute(expr_list *ee) prepare_region_from_maps_intersect(map_list, num_maps); setup_region(); + if (rand_calls > 0) + setup_rand(); /* Parse each expression and initialize the maps, buffers and variables */ @@ -455,6 +488,10 @@ void execute(expr_list *ee) } #endif current_row = (int *)G_malloc(sizeof(int) * threads); + if (rand_calls > 0) { + rand_state = G_malloc(sizeof(*rand_state) * threads); + calc_set_random_state(thread_rand_state); + } count = rows * depths; n = 0; @@ -467,11 +504,17 @@ void execute(expr_list *ee) G_percent(n, count, 2); int tid = 0; + struct G_random_state row_rand_state; #if defined(_OPENMP) tid = omp_get_thread_num(); #endif /* calculate through expressions row by row */ current_row[tid] = row; + if (rand_calls > 0) { + G_random_state_for_unit(&row_rand_state, &rand_layout, + (int64_t)current_depth * rows + row); + rand_state[tid] = &row_rand_state; + } for (i = 0; i < num_exprs; i++) { expression *e = exp_arr[i]; evaluate(e); @@ -530,6 +573,9 @@ void execute(expr_list *ee) /* Free the memory and make it unreachable */ G_free(current_row); + calc_set_random_state(NULL); + G_free(rand_state); + rand_state = NULL; for (i = 0; i < num_exprs; i++) { expression *e = exp_arr[i]; free_buf(e); diff --git a/raster/r.mapcalc/globals.h b/raster/r.mapcalc/globals.h index 6dff18af580..44aa01c825f 100644 --- a/raster/r.mapcalc/globals.h +++ b/raster/r.mapcalc/globals.h @@ -1,9 +1,12 @@ #ifndef __GLOBALS_H_ #define __GLOBALS_H_ +#include + extern int overwrite_flag; -extern long seed_value; +extern int64_t seed_value; extern long seeded; +extern int rand_calls; extern int region_approach; extern int current_depth; diff --git a/raster/r.mapcalc/main.c b/raster/r.mapcalc/main.c index 522fdddde7b..61cd6ec24e9 100644 --- a/raster/r.mapcalc/main.c +++ b/raster/r.mapcalc/main.c @@ -12,11 +12,11 @@ #include #endif +#include #include #include #include #include -#include #include @@ -26,8 +26,9 @@ int overwrite_flag; -long seed_value; +int64_t seed_value; long seeded; +int rand_calls; int region_approach; /****************************************************************************/ @@ -55,40 +56,42 @@ static expr_list *parse_file(const char *filename) return res; } -static bool has_rand_expr(const expression *e) +/* Count the rand() calls of an expression. A variable is not followed, + * since the rand() calls of its value are counted where the variable is + * defined, and they draw once per cell however often the variable is + * used. */ +static int count_rand_calls(const expression *e) { + int count = 0; + if (!e) return 0; switch (e->type) { case expr_type_function: if (strcmp(e->data.func.name, "rand") == 0) - return 1; + count++; // args is 1-indexed (likely from yacc parser conventions) - for (int i = 1; i <= e->data.func.argc; i++) { - if (has_rand_expr(e->data.func.args[i])) - return 1; - } - return 0; + for (int i = 1; i <= e->data.func.argc; i++) + count += count_rand_calls(e->data.func.args[i]); + return count; case expr_type_binding: - return has_rand_expr(e->data.bind.val); - - case expr_type_variable: - return has_rand_expr(e->data.var.bind); + return count_rand_calls(e->data.bind.val); default: return 0; } } -static bool expr_list_has_rand(const expr_list *list) +/* Count the rand() calls of all expressions, which every row evaluates. */ +static int expr_list_count_rand_calls(const expr_list *list) { - for (; list; list = list->next) { - if (has_rand_expr(list->exp)) - return 1; - } - return 0; + int count = 0; + + for (; list; list = list->next) + count += count_rand_calls(list->exp); + return count; } int main(int argc, char **argv) @@ -187,18 +190,18 @@ int main(int argc, char **argv) if (!result) G_fatal_error(_("parse error")); - bool has_rand = expr_list_has_rand(result); + rand_calls = expr_list_count_rand_calls(result); if (seed->answer) { - seed_value = atol(seed->answer); - G_srand48(seed_value); + seed_value = G_random_seed_from_option(seed); seeded = 1; - G_debug(3, "Read random seed from seed=: %ld", seed_value); + G_debug(3, "Read random seed from seed=: %" PRId64, seed_value); } else { - if (has_rand) { - seed_value = G_srand48_auto(); + if (rand_calls > 0) { + seed_value = G_random_generate_seed(); seeded = 1; - G_debug(3, "Automatically generated random seed: %ld", seed_value); + G_debug(3, "Automatically generated random seed: %" PRId64, + seed_value); } if (random->answer) { G_verbose_message(_("Flag 's' is deprecated and will be removed in " @@ -239,12 +242,6 @@ int main(int argc, char **argv) nprocs->answer = "1"; G_verbose_message(_("r3.mapcalc does not support parallel execution.")); } - else if ((threads != 1) && (has_rand)) { - threads = 1; - nprocs->answer = "1"; - G_verbose_message( - _("Parallel execution is not supported with rand() function")); - } /* Ensure the proper number of threads is assigned */ threads = G_set_omp_num_threads(nprocs); diff --git a/raster/r.mapcalc/map.c b/raster/r.mapcalc/map.c index a336e4200d4..382e1697dc2 100644 --- a/raster/r.mapcalc/map.c +++ b/raster/r.mapcalc/map.c @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -770,7 +771,7 @@ void create_history(const char *dst, expression *e) if (seeded) { char buf[RECORD_LEN]; - snprintf(buf, sizeof(buf), "random seed = %ld", seed_value); + snprintf(buf, sizeof(buf), "random seed = %" PRId64, seed_value); Rast_append_history(&hist, buf); } diff --git a/raster/r.mapcalc/r.mapcalc.html b/raster/r.mapcalc/r.mapcalc.html index 34e26c16cc8..0e722095ec4 100644 --- a/raster/r.mapcalc/r.mapcalc.html +++ b/raster/r.mapcalc/r.mapcalc.html @@ -690,20 +690,18 @@

Using the same map for input and output results

Random number generator initialization

-

The pseudo-random number generator used by the rand() function can -be initialised to a specific value using the seed option. -This can be used to replicate a previous calculation. -

Alternatively, it can be initialised from the system time and the -PID using the -s flag. This should result in a different seed -being used each time. -

In either case, the seed will be written to the map's history, and -can be seen using r.info. +

The pseudo-random number generator used by the rand() function can be +initialised to a specific value using the seed parameter. This can be +used to replicate a previous calculation. +

If rand() function is used and no seed is given, +r.mapcalc will generate a seed, resulting in a different result for +each run. +

In either case, the seed will be written to the map's history, and can +be seen using r.info.

If you want other people to be able to verify your results, it's -preferable to use the seed option to supply a seed which is -either specified in the script or generated from a deterministic process -such as a pseudo-random number generator given an explicit seed. -

Note that the rand() function will generate a fatal error if neither -the seed option nor the -s flag are given. +preferable to use the seed parameter. +

The values for a given seed depend on the computational region and on +the number of rand() calls (not on the number of threads, nprocs).

EXAMPLES

@@ -743,8 +741,7 @@

EXAMPLES

-To create a map with random values in a defined range (needs either the -usage of -s flag or the seed parameter). The precision of +To create a map with random values in a defined range. The precision of the input values determines the output precision (the resulting raster map type): diff --git a/raster/r.mapcalc/r.mapcalc.md b/raster/r.mapcalc/r.mapcalc.md index 964fb3f6cbe..1617c8d58bd 100644 --- a/raster/r.mapcalc/r.mapcalc.md +++ b/raster/r.mapcalc/r.mapcalc.md @@ -775,6 +775,9 @@ be seen using *r.info*. If you want other people to be able to verify your results, it's preferable to use the **seed** parameter. +The values for a given seed depend on the computational region and on +the number of rand() calls (not on the number of threads, **nprocs**). + ## EXAMPLES To compute the average of two raster map layers *a* and *b*: @@ -869,7 +872,6 @@ Note: r.mapcalc may disable parallelization in certain cases, even when requeste - When a mask is active, because the current parallel implementation does not support it. -- When the rand() function is used, to ensure reproducible results. ![Benchmark of r.mapcalc](r_mapcalc_benchmark_time.png) *Figure: Benchmark shows execution time for different number of cells diff --git a/raster/r.mapcalc/r3.mapcalc.html b/raster/r.mapcalc/r3.mapcalc.html index 0327d4b76ae..61971c46be7 100644 --- a/raster/r.mapcalc/r3.mapcalc.html +++ b/raster/r.mapcalc/r3.mapcalc.html @@ -483,20 +483,19 @@

3D GRID mask handling

See r3.mask for details.

Random number generator initialization

-

The pseudo-random number generator used by the rand() function can -be initialised to a specific value using the seed option. -This can be used to replicate a previous calculation. -

Alternatively, it can be initialised from the system time and the -PID using the -r flag. This should result in a different seed -being used each time. -

In either case, the seed will be written to the map's history, and -can be seen using r.info. +

The pseudo-random number generator used by the rand() function can be +initialised to a specific value using the seed parameter. This can be +used to replicate a previous calculation. +

If rand() function is used and no seed is given, +r3.mapcalc will generate a seed, resulting in a different result for +each run. +

A generated seed is not written to the history of the 3D raster map, +which records only the command, so give the seed to be able to +replicate a calculation.

If you want other people to be able to verify your results, it's -preferable to use the seed option to supply a seed which is -either specified in the script or generated from a deterministic process -such as a pseudo-random number generator given an explicit seed. -

Note that the rand() function will generate a fatal error if neither -the seed option nor the -s flag are given. +preferable to use the seed parameter. +

The values for a given seed depend on the computational region and on +the number of rand() calls.

EXAMPLES

diff --git a/raster/r.mapcalc/r3.mapcalc.md b/raster/r.mapcalc/r3.mapcalc.md index 20f533226e6..8c9d588458d 100644 --- a/raster/r.mapcalc/r3.mapcalc.md +++ b/raster/r.mapcalc/r3.mapcalc.md @@ -558,12 +558,16 @@ If rand() function is used and no seed is given, *r3.mapcalc* will generate a seed, resulting in a different result for each run. -In either case, the seed will be written to the map's history, and can -be seen using *r.info*. +A generated seed is not written to the history of the 3D raster map, +which records only the command, so give the seed to be able to +replicate a calculation. If you want other people to be able to verify your results, it's preferable to use the **seed** parameter. +The values for a given seed depend on the computational region and on +the number of rand() calls. + ## EXAMPLES To compute the average of two 3D grids *a* and *b*: diff --git a/raster/r.mapcalc/tests/r_mapcalc_nprocs_test.py b/raster/r.mapcalc/tests/r_mapcalc_nprocs_test.py index f6f999992ed..fbc01458a76 100644 --- a/raster/r.mapcalc/tests/r_mapcalc_nprocs_test.py +++ b/raster/r.mapcalc/tests/r_mapcalc_nprocs_test.py @@ -36,16 +36,6 @@ def test_raster_does_not_exist(session_in_mapset, nprocs): assert "does_not_exist" in result.stderr.lower() -@pytest.mark.skip(reason="Waiting for a fix to work on all platforms for any nprocs") -@pytest.mark.parametrize("nprocs", [0, 1, *list(range(2, 11, 2))]) -def test_rand_no_explicit_seed_setting(session_in_mapset, nprocs): - tools = Tools( - session=session_in_mapset, consistent_return_value=True, errors="ignore" - ) - result = tools.r_mapcalc(expression="test = rand(-15.0, 5.0)", nprocs=nprocs) - assert result.returncode == 0 - - @pytest.mark.parametrize("nprocs", [0, 1, 4]) def test_too_many_arguments(session_in_mapset, nprocs): tools = Tools( diff --git a/raster/r.mapcalc/tests/r_mapcalc_rand_layout_test.py b/raster/r.mapcalc/tests/r_mapcalc_rand_layout_test.py new file mode 100644 index 00000000000..50ee277875f --- /dev/null +++ b/raster/r.mapcalc/tests/r_mapcalc_rand_layout_test.py @@ -0,0 +1,172 @@ +# SPDX-FileCopyrightText: 2026 GRASS Development Team +# SPDX-FileCopyrightText: 2026 Vaclav Petras +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Tests of rand() in r.mapcalc and r3.mapcalc drawing from an exact layout + +The expected values are computed with the library's random number layouts: +a row is a unit of an exact layout which draws columns * calls values, so +its stream is the part of the seed's sequence which it draws +when the rows are evaluated one after another. Each rand() call draws one +value per cell of the row, in the order in which the expressions are +evaluated. In 3D, the rows are numbered across the depths, so row r of +depth d is unit d * rows + r. +""" + +from ctypes import byref + +import numpy as np +import pytest + +import grass.script.array as garray +from grass.lib.gis import ( + G_random_double, + G_random_init_layout_exact, + G_random_state_for_unit, + struct_G_random_layout, + struct_G_random_state, +) +from grass.tools import Tools + +ROWS = 7 +COLS = 5 +REGION = {"n": ROWS, "s": 0, "e": COLS, "w": 0, "res": 1} + + +def row_values(seed, rows, cols, calls=1, depths=1): + """Return the values of each row, one array per rand() call + + The arrays have one row per row of every depth, the depths one after + another. + """ + per_row = calls * cols + layout = struct_G_random_layout() + G_random_init_layout_exact(byref(layout), seed, depths * rows, per_row) + state = struct_G_random_state() + values = [] + for unit in range(depths * rows): + G_random_state_for_unit(byref(state), byref(layout), unit) + values.append([G_random_double(byref(state)) for i in range(per_row)]) + values = np.array(values) + return [values[:, call * cols : (call + 1) * cols] for call in range(calls)] + + +def cell_values(values, low, high): + """Return the integers which rand(low, high) makes of the values""" + return low + (values * 2**32).astype(np.int64) % (high - low) + + +@pytest.mark.parametrize("nprocs", [1, 4]) +def test_expressions_draw_in_order(session_in_mapset, nprocs): + """The rand() calls of the expressions draw the sequence row by row + + The expressions draw in their order, and each type of rand() uses the + values in its own way. + """ + tools = Tools(session=session_in_mapset) + tools.g_region(**REGION) + tools.r_mapcalc( + expression=( + "first = rand(0.0, 1.0)\n" + "second = rand(0, 1000)\n" + "third = rand(float(0), float(1))" + ), + seed=42, + nprocs=nprocs, + ) + first, second, third = row_values(42, ROWS, COLS, calls=3) + env = session_in_mapset.env + assert np.array_equal(garray.array("first", env=env), first) + assert np.array_equal(garray.array("second", env=env), cell_values(second, 0, 1000)) + assert np.array_equal(garray.array("third", env=env), third.astype(np.float32)) + + +@pytest.mark.parametrize("nprocs", [1, 4]) +def test_rand_calls_in_order_of_arguments(session_in_mapset, nprocs): + """The rand() calls of one expression draw in the order of arguments""" + tools = Tools(session=session_in_mapset) + tools.g_region(**REGION) + tools.r_mapcalc( + expression="result = rand(0.0, 1.0) + 2 * rand(0.0, 1.0)", + seed=42, + nprocs=nprocs, + ) + first, second = row_values(42, ROWS, COLS, calls=2) + result = garray.array("result", env=session_in_mapset.env) + assert np.array_equal(result, first + 2 * second) + + +@pytest.mark.parametrize("nprocs", [1, 4]) +def test_if_draws_for_both_branches(session_in_mapset, nprocs): + """if() evaluates both branches, so both draw from the sequence""" + tools = Tools(session=session_in_mapset) + tools.g_region(**REGION) + tools.r_mapcalc( + expression="result = if(rand(0, 2), rand(0.0, 1.0), rand(10.0, 11.0))", + seed=42, + nprocs=nprocs, + ) + condition, then, otherwise = row_values(42, ROWS, COLS, calls=3) + expected = np.where( + cell_values(condition, 0, 2) != 0, then, 10.0 + otherwise * (11.0 - 10.0) + ) + result = garray.array("result", env=session_in_mapset.env) + assert np.array_equal(result, expected) + + +@pytest.mark.parametrize("nprocs", [1, 4]) +def test_eval_variable_draws_once(session_in_mapset, nprocs): + """A variable of eval() draws once per cell however often it is used""" + tools = Tools(session=session_in_mapset) + tools.g_region(**REGION) + tools.r_mapcalc( + expression="result = eval(x = rand(0.0, 1.0), x + 2 * rand(0.0, 1.0) + x)", + seed=42, + nprocs=nprocs, + ) + first, second = row_values(42, ROWS, COLS, calls=2) + result = garray.array("result", env=session_in_mapset.env) + assert np.array_equal(result, first + 2 * second + first) + + +def test_r3_rows_continue_sequence(session_in_mapset): + """In 3D, the rows of each depth continue the sequence""" + depths = 3 + tools = Tools(session=session_in_mapset) + tools.g_region(**REGION, t=depths, b=0, res3=1, tbres=1) + tools.r3_mapcalc( + expression="result = rand(0.0, 1.0) + 2 * rand(0.0, 1.0)", + seed=42, + ) + first, second = row_values(42, ROWS, COLS, calls=2, depths=depths) + expected = (first + 2 * second).reshape(depths, ROWS, COLS) + result = garray.array3d("result", env=session_in_mapset.env) + assert np.array_equal(result, expected) + + +@pytest.mark.parametrize("flags", [None, "s"]) +def test_automatic_seed_is_the_reported_seed(session_in_mapset, flags): + """An automatic seed gives the result of the seed written to the history""" + env = session_in_mapset.env.copy() + env["GRASS_RANDOM_SEED"] = "1234" + tools = Tools(env=env) + tools.g_region(**REGION) + tools.r_mapcalc(expression="automatic = rand(0.0, 1.0)", flags=flags) + tools.r_mapcalc(expression="given = rand(0.0, 1.0)", seed=1234) + assert "random seed = 1234" in tools.r_info(map="automatic", flags="h").text + assert np.array_equal( + garray.array("automatic", env=env), garray.array("given", env=env) + ) + + +@pytest.mark.parametrize("seed", [str(2**32), "12abc", "1.5", str(2**64)]) +def test_invalid_seed(session_in_mapset, seed): + """A seed out of range or with trailing characters is an error""" + tools = Tools( + session=session_in_mapset, consistent_return_value=True, errors="ignore" + ) + tools.g_region(**REGION) + result = tools.r_mapcalc(expression="result = rand(0.0, 1.0)", seed=seed) + assert result.returncode == 1 + assert f"Invalid random seed <{seed}>" in result.stderr + assert not tools.g_list(type="raster", pattern="result").text diff --git a/raster/r.mapcalc/tests/r_mapcalc_rand_seed_test.py b/raster/r.mapcalc/tests/r_mapcalc_rand_seed_test.py index 385a9fd362a..a103f78564e 100644 --- a/raster/r.mapcalc/tests/r_mapcalc_rand_seed_test.py +++ b/raster/r.mapcalc/tests/r_mapcalc_rand_seed_test.py @@ -1,8 +1,6 @@ """Tests of r.mapcalc rand() with a given or automatic seed -A given seed must reproduce the reference rasters regardless of nprocs: -r.mapcalc evaluates expressions with rand() single-threaded even -when more processes are requested. +A given seed must reproduce the reference rasters regardless of nprocs. """ import io @@ -121,21 +119,6 @@ def test_seed_fcell(session_in_mapset, nprocs): ) -def test_rand_nprocs_single_thread_message(session_in_mapset): - """r.mapcalc reports that rand() does not support parallel execution. - - The seed tests rely on rand() forcing single-threaded evaluation for - any nprocs; this pins the message reporting it. The message is emitted - at verbose level only. - """ - tools = Tools(session=session_in_mapset, consistent_return_value=True) - tools.g_region(**REGION) - result = tools.r_mapcalc( - expression="rand_nprocs = rand(1, 200)", seed=500, nprocs=4, verbose=True - ) - assert "Parallel execution is not supported with rand()" in result.stderr - - def test_seed_not_required(session_in_mapset): """No seed is needed when rand() is not used.""" tools = Tools(session=session_in_mapset)