Skip to content

Commit 41011c4

Browse files
test-prism: thread-safe deterministic RNG (rand_r + per-thread seeds) (#98)
The point/segment/normal tests call rand() inside OpenMP parallel loops. rand() uses global shared state, so even with a fixed seed the parallel execution order changes which iteration consumes which random value: failures weren't reproducible and varied between the omp=1 and omp=8 CI legs. Thread all random helpers through an 'unsigned *seed' and use rand_r(), with one seed per thread (seed[tid] = tid) passed as &seed[omp_get_thread_num()]. schedule(dynamic) -> schedule(static) fixes the iteration->thread mapping so the per-thread seeds actually yield reproducible results; the failure count is an integer reduction, so it's independent of thread combination order. The geometry calls still run in parallel, preserving thread-safety coverage. rand()/srand are removed; non-OpenMP builds get omp_get_* stubs.
1 parent 5a460ed commit 41011c4

1 file changed

Lines changed: 58 additions & 28 deletions

File tree

utils/test-prism.c

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131

3232
#ifdef _OPENMP
3333
#include <omp.h>
34+
#else
35+
static int omp_get_thread_num(void) { return 0; }
36+
static int omp_get_max_threads(void) { return 1; }
3437
#endif
3538

3639
#include "ctlgeom.h"
@@ -117,59 +120,73 @@ static vector3 make_vector3(double x, double y, double z) {
117120
/************************************************************************/
118121
/* return a uniform random number in [a,b] */
119122
/************************************************************************/
120-
static double urand(double a, double b) { return a + (b - a) * (rand() / ((double)RAND_MAX)); }
123+
/* Thread-safe: each thread passes a pointer to its own seed (see rand_r),
124+
* so results are reproducible and independent of parallel execution order. */
125+
static double urand(double a, double b, unsigned int *seed) {
126+
return a + (b - a) * (rand_r(seed) / ((double)RAND_MAX));
127+
}
128+
129+
static double drand(unsigned int *seed) { return urand(0.0, 1.0, seed); }
121130

122-
static double drand() { return urand(0.0, 1.0); }
131+
/* Allocate one PRNG seed per OpenMP thread, seed[tid] = tid (caller frees).
132+
* Inside a parallel loop, pass &seed[omp_get_thread_num()] to the random
133+
* helpers so each thread draws from its own reproducible, thread-safe stream. */
134+
static unsigned int *make_thread_seeds(void) {
135+
int nthreads = omp_get_max_threads(), t;
136+
unsigned int *seed = (unsigned int *)malloc(nthreads * sizeof(unsigned int));
137+
for (t = 0; t < nthreads; t++) seed[t] = (unsigned int)t;
138+
return seed;
139+
}
123140

124141
/************************************************************************/
125142
/* random point uniformly distributed over a parallelepiped */
126143
/************************************************************************/
127-
vector3 random_point_in_box(vector3 min_corner, vector3 max_corner) {
128-
return make_vector3(urand(min_corner.x, max_corner.x), urand(min_corner.y, max_corner.y),
129-
urand(min_corner.z, max_corner.z));
144+
vector3 random_point_in_box(vector3 min_corner, vector3 max_corner, unsigned int *seed) {
145+
return make_vector3(urand(min_corner.x, max_corner.x, seed), urand(min_corner.y, max_corner.y, seed),
146+
urand(min_corner.z, max_corner.z, seed));
130147
}
131148

132149
/************************************************************************/
133150
/* random point uniformly distributed over a planar polygon */
134151
/* (all z coordinates are 0) */
135152
/************************************************************************/
136-
vector3 random_point_in_polygon(prism *prsm) {
153+
vector3 random_point_in_polygon(prism *prsm, unsigned int *seed) {
137154
// randomly choose a vertex and generate random point within the triangle
138155
// formed by that vertex, the next vertex, and the centroid
139156
vector3 *vertices = prsm->vertices.items;
140157
int num_vertices = prsm->vertices.num_items;
141-
int which_vertex = rand() % num_vertices;
158+
int which_vertex = rand_r(seed) % num_vertices;
142159
vector3 v0 = {0, 0, 0};
143160
vector3 v1 = vertices[which_vertex];
144161
vector3 v2 = vertices[(which_vertex + 1) % num_vertices];
145-
double xi = urand(0.0, 1.0), eta = urand(0.0, 1.0 - xi);
162+
double xi = urand(0.0, 1.0, seed), eta = urand(0.0, 1.0 - xi, seed);
146163
return vector3_plus(vector3_scale(xi, vector3_minus(v1, v0)),
147164
vector3_scale(eta, vector3_minus(v2, v0)));
148165
}
149166

150167
/************************************************************************/
151168
/* random point uniformly distributed over the surface of a prism */
152169
/************************************************************************/
153-
vector3 random_point_on_prism(geometric_object o) {
170+
vector3 random_point_on_prism(geometric_object o, unsigned int *seed) {
154171
prism *prsm = o.subclass.prism_data;
155172
vector3 *vertices = prsm->vertices_p.items;
156173
int num_vertices = prsm->vertices_p.num_items;
157174
double height = prsm->height;
158175

159176
// choose a face
160177
int num_faces = num_vertices + 2;
161-
int which_face = rand() % num_faces;
178+
int which_face = rand_r(seed) % num_faces;
162179
if (which_face < num_vertices) // side face
163180
{
164181
vector3 min_corner = vertices[which_face];
165182
vector3 max_corner = vertices[(which_face + 1) % num_vertices];
166183
max_corner.z = height;
167184
return random_point_in_box(prism_coordinate_p2c(prsm, min_corner),
168-
prism_coordinate_p2c(prsm, max_corner));
185+
prism_coordinate_p2c(prsm, max_corner), seed);
169186
}
170187
else // floor or ceiling
171188
{
172-
vector3 p = random_point_in_polygon(prsm);
189+
vector3 p = random_point_in_polygon(prsm, seed);
173190
if (which_face == num_faces - 1) p.z = height;
174191
return prism_coordinate_p2c(prsm, p);
175192
}
@@ -178,9 +195,9 @@ vector3 random_point_on_prism(geometric_object o) {
178195
/************************************************************************/
179196
/* random unit vector with direction uniformly distributed over unit sphere*/
180197
/************************************************************************/
181-
vector3 random_unit_vector3() {
182-
double cos_theta = urand(0.0, 1.0), sin_theta = sqrt(1.0 - cos_theta * cos_theta);
183-
double phi = urand(0.0, 2.0 * K_PI);
198+
vector3 random_unit_vector3(unsigned int *seed) {
199+
double cos_theta = urand(0.0, 1.0, seed), sin_theta = sqrt(1.0 - cos_theta * cos_theta);
200+
double phi = urand(0.0, 2.0 * K_PI, seed);
184201
return make_vector3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta);
185202
}
186203

@@ -284,9 +301,13 @@ int test_point_inclusion(geometric_object the_block, geometric_object the_prism,
284301
// latter calls geom_fix_object_ptr internally and is not thread-safe.
285302
// the_block and the_prism were fixed up in run_unit_tests above
286303
// before this parallel loop runs.
287-
#pragma omp parallel for schedule(dynamic) reduction(+ : num_failed, num_adjusted)
304+
// schedule(static): fixed iteration->thread mapping so per-thread seeds give
305+
// reproducible results (dynamic scheduling would reintroduce nondeterminism).
306+
unsigned int *seed = make_thread_seeds();
307+
#pragma omp parallel for schedule(static) reduction(+ : num_failed, num_adjusted)
288308
for (n = 0; n < num_tests; n++) {
289-
vector3 p = random_point_in_box(min_corner, max_corner);
309+
unsigned int *s = &seed[omp_get_thread_num()];
310+
vector3 p = random_point_in_box(min_corner, max_corner, s);
290311
boolean in_block = point_in_fixed_objectp(p, the_block);
291312
boolean in_prism = point_in_fixed_objectp(p, the_prism);
292313

@@ -304,6 +325,7 @@ int test_point_inclusion(geometric_object the_block, geometric_object the_prism,
304325
}
305326
}
306327
if (f) fclose(f);
328+
free(seed);
307329

308330
printf("point inclusion: %i/%i points failed (%i adjusted)\n", num_failed, num_tests,
309331
num_adjusted);
@@ -325,14 +347,16 @@ int test_normal_to_object(geometric_object the_block, geometric_object the_prism
325347
double tolerance = 1.0e-6;
326348

327349
int n;
328-
#pragma omp parallel for schedule(dynamic) reduction(+ : num_failed)
350+
unsigned int *seed = make_thread_seeds();
351+
#pragma omp parallel for schedule(static) reduction(+ : num_failed)
329352
for (n = 0; n < num_tests; n++) {
353+
unsigned int *s = &seed[omp_get_thread_num()];
330354
// with probability PFACE, generate random base point lying on one
331355
// of the 6 faces of the prism.
332356
// with probability 1-PFACE, generate random base point lying in the
333357
// extended volume (2x volume of block)
334-
vector3 p = (urand(0.0, 1.0) < PFACE) ? random_point_on_prism(the_prism)
335-
: random_point_in_box(min_corner, max_corner);
358+
vector3 p = (urand(0.0, 1.0, s) < PFACE) ? random_point_on_prism(the_prism, s)
359+
: random_point_in_box(min_corner, max_corner, s);
336360

337361
// normal_to_fixed_object instead of normal_to_object: the latter is
338362
// not thread-safe (calls geom_fix_object_ptr internally).
@@ -348,6 +372,7 @@ int test_normal_to_object(geometric_object the_block, geometric_object the_prism
348372
}
349373
}
350374
if (f) fclose(f);
375+
free(seed);
351376

352377
printf("%i/%i normals failed\n", num_failed, num_tests);
353378
return num_failed;
@@ -365,13 +390,15 @@ int test_line_segment_intersection(geometric_object the_block, geometric_object
365390

366391
int num_failed = 0;
367392
int n;
368-
#pragma omp parallel for schedule(dynamic) reduction(+ : num_failed)
393+
unsigned int *seed = make_thread_seeds();
394+
#pragma omp parallel for schedule(static) reduction(+ : num_failed)
369395
for (n = 0; n < num_tests; n++) {
396+
unsigned int *s = &seed[omp_get_thread_num()];
370397
// randomly generated base point within enlarged bounding box
371-
vector3 p = random_point_in_box(min_corner, max_corner);
372-
vector3 d = random_unit_vector3();
373-
double a = urand(0.0, 1.0);
374-
double b = urand(0.0, 1.0);
398+
vector3 p = random_point_in_box(min_corner, max_corner, s);
399+
vector3 d = random_unit_vector3(s);
400+
double a = urand(0.0, 1.0, s);
401+
double b = urand(0.0, 1.0, s);
375402

376403
double sblock = intersect_line_segment_with_object(p, d, the_block, a, b);
377404
double sprism = intersect_line_segment_with_object(p, d, the_prism, a, b);
@@ -394,6 +421,7 @@ int test_line_segment_intersection(geometric_object the_block, geometric_object
394421
}
395422
}
396423
if (f) fclose(f);
424+
free(seed);
397425

398426
printf("%i/%i segments failed\n", num_failed, num_tests);
399427
return num_failed;
@@ -1154,8 +1182,9 @@ int run_unit_tests() {
11541182
/* and prism by a random displacement vector */
11551183
/***************************************************************/
11561184
#define P_SHIFT 0.75
1157-
if (urand(0.0, 1.0) < P_SHIFT) {
1158-
vector3 shift = vector3_scale(urand(0.0, 1.0), random_unit_vector3());
1185+
unsigned int shift_seed = 0; // single-threaded setup, own seed
1186+
if (urand(0.0, 1.0, &shift_seed) < P_SHIFT) {
1187+
vector3 shift = vector3_scale(urand(0.0, 1.0, &shift_seed), random_unit_vector3(&shift_seed));
11591188
the_block.center = vector3_plus(the_block.center, shift);
11601189
the_prism.center = vector3_plus(the_prism.center, shift);
11611190
// Sync each object's internal cache (block projection_matrix, prism
@@ -1211,7 +1240,8 @@ void usage(char *msg) { print_usage(msg, 1); }
12111240
/************************************************************************/
12121241
/************************************************************************/
12131242
int main(int argc, char *argv[]) {
1214-
srand(time(NULL));
1243+
// No global srand(): the randomized tests use rand_r with fixed per-thread
1244+
// seeds (see make_thread_seeds), so runs are deterministic and reproducible.
12151245
geom_initialize();
12161246

12171247
if (argc <= 1) // if no arguments, run unit tests

0 commit comments

Comments
 (0)