Skip to content

libgis: Add integer functions for a random number state - #8079

Merged
wenzeslaus merged 10 commits into
OSGeo:mainfrom
wenzeslaus:random-int
Oct 20, 2026
Merged

wenzeslaus merged 10 commits into
OSGeo:mainfrom
wenzeslaus:random-int

Conversation

@wenzeslaus

Copy link
Copy Markdown
Member

Since I focused the original implementation of the new RNG on r.sim.water, the caller-owned struct G_random_state could be used with G_random_double(). This adds two integer functions, replacing G_lrand48() and G_mrand48() from the global generator (the int and int32 functions). It also add a new uint32 function which complements the int32 variant.

As with the double function, this gives the same results as the global functions at this point (and that's tested).

Details (AI generated)

The functions

One draw of the state read three ways, each with its range in the doc comment and the old function it reproduces:

function range same values as
int G_random_int(struct G_random_state *) [0, 2^31) G_lrand48()
int32_t G_random_int32(struct G_random_state *) [-2^31, 2^31) G_mrand48()
uint32_t G_random_uint32(struct G_random_state *) [0, 2^32) (unsigned int)G_mrand48(), the floor of 2^32 times G_random_double()

G_random_int() returns a plain int, as G_random_double() returns a plain double: a value to compute with, whose range the documentation gives; the two fixed-width functions are the bit-level pair. The old functions returned long from the days when int could be 16 bits; int holds 2^31 - 1 on every platform GRASS builds on.

Migration

before after
G_lrand48() G_random_int(&rng)
G_mrand48() G_random_int32(&rng)
(unsigned int)G_mrand48() G_random_uint32(&rng)

Tests

One test with 100 draws for each of seven seeds checks all four relations against the shared generator and the double.

Suggested squash commit message

libgis: Add integer functions for a random number state
The caller-owned G_random_state could be drawn from only as a double.
G_random_int() and G_random_int32() give what G_lrand48() and
G_mrand48() give from the global generator, so code moving to its own
state keeps its integer values too. G_random_uint32() complements
G_random_int32() with the same bits read as unsigned. The migration
table of the random numbers page points at the new functions. A test
checks the relations to the global functions, which hold for the
current engine.

The code and tests were written with Claude Code (Claude Fable 5.1) and reviewed by the author of the PR.

G_random_double() was the only way to draw from a program-owned state,
so code which needed an integer, as from G_lrand48() or G_mrand48(),
scaled the double by 2^32 and cast it, repeating the constant and the
cast at every site. The three functions are one draw read three ways:
G_random_uint32() gives the high 32 bits of the state, which is what
G_mrand48() returns read as unsigned and the floor of 2^32 times the
double; G_random_int32() reads the same bits signed, as G_mrand48();
G_random_int() keeps their top 31 bits as a non-negative int, as
G_lrand48(). A test checks all four relations for 100 draws of seven
seeds.

Written with the help of Claude Code.
The old functions each one reproduces are named in the migration table
of the random numbers page instead.

Written with the help of Claude Code.
The rows for G_lrand48() and G_mrand48() name G_random_int(),
G_random_int32() and G_random_uint32() instead of scaling and casting
the double by hand.

Written with the help of Claude Code.
The relation to G_random_double() and the fact that the three are one
draw read three ways are for the commit message and the migration table,
not the function comments.

@petrasovaa petrasovaa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nitpicks if you want to look at it, but I think it's good to go.

Details 1. Signed cast is implementation-defined (lib/gis/lrand48.c:834). The (int32_t) cast of a value above INT32_MAX is implementation-defined before C23, but the doc comment promises the bits "read as a signed value" and the test pins it to G_mrand48(). All GRASS targets are two's complement so it matches today, and G_mrand48() already has the same pattern. The contract could be stated as modular conversion, or spelled with an explicit subtraction of 2^32. 2. Narrative doc never mentions the integer functions (lib/gis/random_streams.dox.md:30). The Terms section still says a State is only drawn from by G_random_double(). The three new functions appear only in the migration table. Terms and Usage should mention the integer variants once. 3. Stale G_random_advance() comment (lib/gis/lrand48.c:751). It still says the next G_random_double() returns the skipped draw. Should say "the next draw" now that other functions step the state too. 4. Migration row for G_mrand48() overstates equivalence (random_streams.dox.md:422). It says values are "the same" for both the signed and unsigned replacements, but the unsigned one gives the same bits, not the same value, for half the range. The old row's "same bits" distinction should be kept, or the row split. 5. Naming trap between G_random_int() and G_random_int32() (include/grass/defs/gis.h:535). One returns non-negative [0, 2^31), the other signed [-2^31, 2^31). Near-identical names with different sign contracts. A one-line note in the header, or a name like G_random_int31(), would help. Low severity, API design. 6. Inconsistent doc comment detail (lrand48.c:805). The new draw functions drop the context the G_random_double() comment gives about seeding and obtaining a state. Cosmetic. 7. Test checks only against the live generator (lib/gis/tests/lib_gis_lrand48_test.py:507). The new test compares state-based integer draws to the shared generator but not to the pinned REFERENCE table, unlike the existing drand48 cross-check. Low severity, since the separate G_lrand48/G_mrand48 reference tests cover it indirectly.

@wenzeslaus
wenzeslaus merged commit 50a7d06 into OSGeo:main Oct 20, 2026
26 checks passed
@wenzeslaus

Copy link
Copy Markdown
Member Author

Thanks. I will circle back to some of the nitpics later. 2-4, 6 and 7 are all basically about improving documentation and bringing the integer functions on par with the double ones (in terms of tests and doc). (1 is the current state, so no action. 5 is either doc or name choice, I considered different combinations and landed on the one in the PR because types are real types unlike the int31 which is a range, but not a type unlike int32.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C Related code is in C docs libraries markdown Related to markdown, markdown files Python Related code is in Python tests Related to Test Suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants