Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9393faa
libgis: Add random number layouts and generator states a program owns
wenzeslaus Sep 30, 2026
7888ba0
libgis: Test the environment seed in a subprocess
wenzeslaus Sep 30, 2026
c8c87e2
libgis: Add an author and license lines for the random number layouts
wenzeslaus Sep 30, 2026
01a5869
libgis: State the condition for thread-safe program-owned generators
wenzeslaus Sep 30, 2026
5f2cd1a
libgis: Share the seed range test between the seed checks
wenzeslaus Sep 30, 2026
8526522
libgis: State the overflow check as a precondition of init_layout()
wenzeslaus Sep 30, 2026
0b55fd3
r.sim: Give each walker random numbers of its own
wenzeslaus Sep 30, 2026
1cf8a00
r.sim: Add copyright lines to the random number tests
wenzeslaus Sep 30, 2026
efa774c
Merge remote-tracking branch 'upstream/main' into rsim-layouts
wenzeslaus Oct 1, 2026
7c0d776
r.sim: Keep each walker's random number state in its record
wenzeslaus Oct 1, 2026
cf9f2fe
libgis: Rename the random number seeding, whole-span and batch functions
wenzeslaus Oct 1, 2026
bc46c1e
Merge branch 'random-layouts' into rsim-layouts
wenzeslaus Oct 1, 2026
83a4c95
r.sim: Follow the renamed random number functions
wenzeslaus Oct 1, 2026
dfd0c6b
libgis: Start the batches of random numbers an odd number of draws apart
wenzeslaus Oct 2, 2026
f30708d
Merge branch 'random-layouts' into rsim-layouts
wenzeslaus Oct 2, 2026
5fd1ddf
libgis: Use uint_least64_t for the state of the program-owned generators
wenzeslaus Oct 2, 2026
373a489
Merge branch 'random-layouts' into rsim-layouts
wenzeslaus Oct 2, 2026
eebd877
libgis: Use exact-width integers inside the program-owned generators
wenzeslaus Oct 2, 2026
f5bfcbd
Merge branch 'random-layouts' into rsim-layouts
wenzeslaus Oct 2, 2026
9bb7cf7
libgis: Make the whole-span flag of a random number layout a bool
wenzeslaus Oct 2, 2026
09ee024
Merge branch 'random-layouts' into rsim-layouts
wenzeslaus Oct 2, 2026
fe5f83f
libgis: Take and return int64_t in the random number layout functions
wenzeslaus Oct 3, 2026
e0c75e4
Merge branch 'random-layouts' into rsim-layouts
wenzeslaus Oct 3, 2026
512d6da
r.sim: Hold the seed as int64_t
wenzeslaus Oct 3, 2026
5554480
Merge remote-tracking branch 'upstream/main' into rsim-layouts
wenzeslaus Oct 4, 2026
96cd2a0
r.sim.water: Describe the locked infiltration in the nprocs note
wenzeslaus Oct 4, 2026
9599115
r.sim: Drop comments which restate the code
wenzeslaus Oct 5, 2026
1ac082e
r.sim: Generate the seed when none is given and record it
wenzeslaus Oct 5, 2026
e64d51b
r.sim: Include stdint.h where the seed is held as int64_t
wenzeslaus Oct 5, 2026
9bbed54
r.sim: Give a walker's stream room for one step's run of bad luck
wenzeslaus Oct 5, 2026
0303ff6
r.sim.water: Run the flow control test with a thousand walkers
wenzeslaus Oct 5, 2026
73d86a0
r.sim: Record the seed under the key seed
wenzeslaus Oct 5, 2026
1bb9d0f
r.sim.water: Name the cases the flow control test separates
wenzeslaus Oct 5, 2026
d831e04
r.sim: Drop two forward declarations hydro.c does not need
wenzeslaus Oct 5, 2026
a44ccaa
r.sim: Say what the random numbers mean for the user in the tool docs
wenzeslaus Oct 6, 2026
c168719
r.sim: Document results with more than one thread as not reproducible
wenzeslaus Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 8 additions & 25 deletions raster/r.sim/r.sim.sediment/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
/* INCLUDES */

/********************************/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand Down Expand Up @@ -102,7 +103,6 @@ int main(int argc, char *argv[])
struct Cell_head cellhd;
struct options parm;
struct flags flag;
long seed_value;

G_gisinit(argv[0]);

Expand Down Expand Up @@ -279,21 +279,16 @@ int main(int argc, char *argv[])
parm.diffc->description = _("Water diffusion constant");
parm.diffc->guisection = _("Parameters");

parm.seed = G_define_option();
parm.seed = G_define_standard_option(G_OPT_M_SEED);
parm.seed->key = "random_seed";
parm.seed->type = TYPE_INTEGER;
parm.seed->required = NO;
parm.seed->label = _("Seed for random number generator");
parm.seed->description =
_("The same seed can be used to obtain same results"
" or random seed can be generated by other means.");

flag.generateSeed = G_define_flag();
flag.generateSeed->key = 's';
flag.generateSeed->label = _("Generate random seed");
flag.generateSeed->label =
_("Generate random seed (result is non-deterministic) [deprecated]");
flag.generateSeed->description =
_("Automatically generates random seed for random number"
" generator (use when you don't want to provide the seed option)");
_("This flag is deprecated and will be removed in a future release. "
"Seeding is automatic or use parameter random_seed.");

parm.threads = G_define_option();
parm.threads->key = "nprocs";
Expand Down Expand Up @@ -329,20 +324,7 @@ int main(int argc, char *argv[])
? SUMMARY_JSON
: SUMMARY_PLAIN;

if (flag.generateSeed->answer) {
seed_value = G_srand48_auto();
G_verbose_message(_("Generated random seed (-s): %ld"), seed_value);
}
else if (parm.seed->answer) {
seed_value = atol(parm.seed->answer);
G_srand48(seed_value);
G_verbose_message(_("Read random seed from %s option: %ld"),
parm.seed->key, seed_value);
}
else {
/* default as it used to be */
G_srand48(12345);
}
int64_t seed = simwe_seed(parm.seed, flag.generateSeed);

G_get_set_window(&cellhd);

Expand All @@ -353,6 +335,7 @@ int main(int argc, char *argv[])
ObservationPoints points = {0};
settings.hhmax = settings.halpha = settings.hbeta = 0;
settings.ts = false;
settings.seed = seed;
Inputs inputs = {0};
Outputs outputs = {0};
Grids grids = {0};
Expand Down
6 changes: 4 additions & 2 deletions raster/r.sim/r.sim.sediment/r.sim.sediment.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ <h2>DESCRIPTION</h2>
<br>

<p>
Increasing the number of threads with <b>nprocs</b> does not really
speed up the simulation.
Increasing the number of threads with <b>nprocs</b> speeds up the
simulation. The random numbers, the seed and the reproducibility of the
results with more than one thread are as in <em>r.sim.water</em>.

<h2>NOTES</h2>

Expand Down Expand Up @@ -93,6 +94,7 @@ <h3>Run summary</h3>
"walkers_requested": 60,
"walkers_generated": 78,
"walkers_remaining": 43,
"seed": 1,
"duration": 60,
"simulated_time": 54.891343113611583,
"time_step": 5.4891343113611581,
Expand Down
6 changes: 4 additions & 2 deletions raster/r.sim/r.sim.sediment/r.sim.sediment.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ controlled by *duration* \[minutes\] parameter. If the resulting
erosion/deposition map is noisy, higher number of walkers, given by
*nwalkers* should be used.

Increasing the number of threads with **nprocs** does not really speed
up the simulation.
Increasing the number of threads with **nprocs** speeds up the
simulation. The random numbers, the seed and the reproducibility of the
results with more than one thread are as in *r.sim.water*.

## NOTES

Expand Down Expand Up @@ -83,6 +84,7 @@ r.sim.sediment elevation=elevation water_depth=water_depth detachment_coeff=deta
"walkers_requested": 60,
"walkers_generated": 78,
"walkers_remaining": 43,
"seed": 1,
"duration": 60,
"simulated_time": 54.891343113611583,
"time_step": 5.4891343113611581,
Expand Down
117 changes: 117 additions & 0 deletions raster/r.sim/r.sim.sediment/tests/r_sim_sediment_random_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# SPDX-FileCopyrightText: 2026 GRASS Development Team
# SPDX-License-Identifier: GPL-2.0-or-later

"""Tests of the random numbers of r.sim.sediment.

Each walker draws from a random number state of its own, set by the seed and
the walker's number, so its random numbers do not depend on the number of
threads. The sediment flux still does at nprocs > 1, because walkers in the
same cell add to the concentration grid without synchronization, so the test
which compares numbers of threads compares the walker positions instead. A
walker's path depends only on the slope and its random numbers.
"""

import os
from io import StringIO

import numpy as np
import pytest

import grass.script as gs
from grass.exceptions import CalledModuleError
from grass.tools import Tools


@pytest.fixture(scope="module")
def session(tmp_path_factory):
"""Session in an XY project with a bowl as elevation and constant inputs."""
project = tmp_path_factory.mktemp("r_sim_sediment_random") / "xy_test"
gs.create_project(project)
with (
gs.setup.init(project, env=os.environ.copy()) as session,
Tools(session=session) as tools,
):
tools.g_region(s=0, n=40, w=0, e=40, res=1)
tools.r_mapcalc(
expression="elevation = 0.01 * ((col() - 20.5)^2 + (row() - 20.5)^2)"
)
tools.r_mapcalc(expression="water_depth = 0.1")
tools.r_mapcalc(expression="detachment = 0.001")
tools.r_mapcalc(expression="transport = 0.001")
tools.r_mapcalc(expression="shear_stress = 0")
yield session


def simulate(session, walkers=None, env=None, **kwargs):
"""Run the simulation, return the flux and the walker coordinates."""
tools = Tools(env=env) if env else Tools(session=session)
if walkers:
kwargs["walkers_output"] = walkers
flux = tools.r_sim_sediment(
elevation="elevation",
water_depth="water_depth",
detachment_coeff="detachment",
transport_coeff="transport",
shear_stress="shear_stress",
sediment_flux=np.array,
duration=2,
**kwargs,
)
if not walkers:
return flux, None
text = tools.v_out_ascii(input=walkers, format="point", precision=15).text
return flux, np.loadtxt(StringIO(text), delimiter="|")


def test_same_seed_gives_same_flux(session):
"""Two runs with the same seed give the same sediment flux."""
first, _ = simulate(session, random_seed=1)
second, _ = simulate(session, random_seed=1)
assert np.array_equal(first, second)


def test_different_seeds_give_different_results(session):
"""Runs with different seeds give different fluxes and walker positions."""
first_flux, first_walkers = simulate(session, "walkers_seed_1", random_seed=1)
second_flux, second_walkers = simulate(session, "walkers_seed_2", random_seed=2)
assert not np.array_equal(first_flux, second_flux)
assert not np.array_equal(first_walkers, second_walkers)


def test_generated_seed_is_the_seed_option(session):
"""Without a seed, GRASS_RANDOM_SEED gives what the seed option gives."""
env = session.env.copy()
env["GRASS_RANDOM_SEED"] = "3"
generated, _ = simulate(session, env=env)
given, _ = simulate(session, random_seed=3)
assert np.array_equal(generated, given)


@pytest.mark.parametrize("nprocs", [2, 4])
def test_results_do_not_depend_on_nprocs(session, nprocs):
"""The flux and the walkers are identical with any number of threads.

The water depth is an input here, so nothing a walker reacts to is
changed by the other walkers of the same step.
"""
serial_flux, serial = simulate(session, f"walkers_{nprocs}_serial", random_seed=5)
parallel_flux, parallel = simulate(
session, f"walkers_{nprocs}_parallel", random_seed=5, nprocs=nprocs
)
assert serial.size
assert np.array_equal(parallel, serial)
assert np.array_equal(parallel_flux, serial_flux)


@pytest.mark.parametrize("seed", [-(2**31) - 1, 2**32])
def test_seed_outside_range_is_an_error(session, seed):
"""A seed the generator cannot use is refused, not silently wrapped."""
with pytest.raises(CalledModuleError, match="outside the range"):
simulate(session, random_seed=seed)


@pytest.mark.parametrize("seed", ["12abc", "1.5", "-", "99999999999999999999"])
def test_seed_which_is_not_an_integer_is_an_error(session, seed):
"""A seed which the parser lets through but which is not an integer is refused."""
with pytest.raises(CalledModuleError, match="Invalid random seed"):
simulate(session, random_seed=seed)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"walkers_requested",
"walkers_generated",
"walkers_remaining",
"seed",
"duration",
"simulated_time",
"time_step",
Expand Down
Binary file modified raster/r.sim/r.sim.sediment/testsuite/data/reference_erdep.pack
Binary file not shown.
Binary file modified raster/r.sim/r.sim.sediment/testsuite/data/reference_sedflux.pack
Binary file not shown.
33 changes: 8 additions & 25 deletions raster/r.sim/r.sim.water/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
/* INCLUDES */

/********************************/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand Down Expand Up @@ -102,7 +103,6 @@ int main(int argc, char *argv[])
struct Cell_head cellhd;
struct options parm;
struct flags flag;
long seed_value;

G_gisinit(argv[0]);

Expand Down Expand Up @@ -314,21 +314,16 @@ int main(int argc, char *argv[])
flag.tserie->description = _("Time-series output");
flag.tserie->guisection = _("Output");

parm.seed = G_define_option();
parm.seed = G_define_standard_option(G_OPT_M_SEED);
parm.seed->key = "random_seed";
parm.seed->type = TYPE_INTEGER;
parm.seed->required = NO;
parm.seed->label = _("Seed for random number generator");
parm.seed->description =
_("The same seed can be used to obtain same results"
" or random seed can be generated by other means.");

flag.generateSeed = G_define_flag();
flag.generateSeed->key = 's';
flag.generateSeed->label = _("Generate random seed");
flag.generateSeed->label =
_("Generate random seed (result is non-deterministic) [deprecated]");
flag.generateSeed->description =
_("Automatically generates random seed for random number"
" generator (use when you don't want to provide the seed option)");
_("This flag is deprecated and will be removed in a future release. "
"Seeding is automatic or use parameter random_seed.");

parm.threads = G_define_option();
parm.threads->key = "nprocs";
Expand Down Expand Up @@ -365,20 +360,7 @@ int main(int argc, char *argv[])
? SUMMARY_JSON
: SUMMARY_PLAIN;

if (flag.generateSeed->answer) {
seed_value = G_srand48_auto();
G_verbose_message(_("Generated random seed (-s): %ld"), seed_value);
}
else if (parm.seed->answer) {
seed_value = atol(parm.seed->answer);
G_srand48(seed_value);
G_verbose_message(_("Read random seed from %s option: %ld"),
parm.seed->key, seed_value);
}
else {
/* default as it used to be */
G_srand48(12345);
}
int64_t seed = simwe_seed(parm.seed, flag.generateSeed);

G_get_set_window(&cellhd);

Expand Down Expand Up @@ -417,6 +399,7 @@ int main(int argc, char *argv[])
G_debug(3, "xmax: %f, ymax: %f", geometry.xmax, geometry.ymax);

settings.ts = flag.tserie->answer;
settings.seed = seed;

inputs.elevin = parm.elevin->answer;
inputs.dxin = parm.dxin->answer;
Expand Down
12 changes: 12 additions & 0 deletions raster/r.sim/r.sim.water/r.sim.water.html
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ <h3>Run summary</h3>
<tr><td><code>walkers_requested</code></td><td>Number of walkers from <b>nwalkers</b>, by default twice the number of cells</td><td>count</td></tr>
<tr><td><code>walkers_generated</code></td><td>Walkers created, at least one per cell and more where the source rate is higher</td><td>count</td></tr>
<tr><td><code>walkers_remaining</code></td><td>Walkers still in the domain at the end of the run</td><td>count</td></tr>
<tr><td><code>seed</code></td><td>Seed of the random numbers, given or generated</td><td></td></tr>
<tr><td><code>duration</code></td><td>Requested simulation length (<b>duration</b>)</td><td>s</td></tr>
<tr><td><code>simulated_time</code></td><td>Simulated time reached at the end of the run</td><td>s</td></tr>
<tr><td><code>time_step</code></td><td>Simulated time per iteration</td><td>s</td></tr>
Expand Down Expand Up @@ -286,6 +287,7 @@ <h3>Run summary</h3>
"walkers_requested": 100000,
"walkers_generated": 120000,
"walkers_remaining": 112724,
"seed": 3,
"duration": 1200,
"simulated_time": 1199.2085202681737,
"time_step": 1.0631281208051186,
Expand Down Expand Up @@ -320,6 +322,16 @@ <h3>Run summary</h3>
}
</pre></div>

<h3>Random numbers and parallel processing</h3>

The walkers are placed and moved using pseudo-random numbers. The seed
is given by <b>random_seed</b>; without it, a seed is generated and
recorded in the history of the output maps and in the run summary as
<code>seed</code>, so that the run can be repeated. With more than one thread, the
results differ slightly between thread counts and between repeated runs,
since the order in which the walkers reach a cell depends on the
threads. Use <b>nprocs=1</b> when results must be reproducible.

<h2>EXAMPLE</h2>

This example uses the
Expand Down
12 changes: 12 additions & 0 deletions raster/r.sim/r.sim.water/r.sim.water.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ stored in the history of the output raster maps under the same keys (see
| `walkers_requested` | Number of walkers from **nwalkers**, by default twice the number of cells | count |
| `walkers_generated` | Walkers created, at least one per cell and more where the source rate is higher | count |
| `walkers_remaining` | Walkers still in the domain at the end of the run | count |
| `seed` | Seed of the random numbers, given or generated | |
| `duration` | Requested simulation length (**duration**) | s |
| `simulated_time` | Simulated time reached at the end of the run | s |
| `time_step` | Simulated time per iteration | s |
Expand Down Expand Up @@ -277,6 +278,7 @@ The printed summary:
"walkers_requested": 100000,
"walkers_generated": 120000,
"walkers_remaining": 112724,
"seed": 3,
"duration": 1200,
"simulated_time": 1199.2085202681737,
"time_step": 1.0631281208051186,
Expand Down Expand Up @@ -311,6 +313,16 @@ The printed summary:
}
```

### Random numbers and parallel processing

The walkers are placed and moved using pseudo-random numbers. The seed
is given by **random_seed**; without it, a seed is generated and
recorded in the history of the output maps and in the run summary as
`seed`, so that the run can be repeated. With more than one thread, the
results differ slightly between thread counts and between repeated runs,
since the order in which the walkers reach a cell depends on the
threads. Use **nprocs=1** when results must be reproducible.

## EXAMPLE

This example uses the
Expand Down
Loading
Loading