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/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/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 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)