Repository navigation
Synthetic turbulence: draw modes from an exact integer PRNG so seeds are independent - #1946
sbryngelson wants to merge 3 commits into
Conversation
…are independent s_prng/modmul round the LCG state to 1e-5 every step, which makes the map many-to-one: every seed tested falls onto the same 149-value cycle within 50-521 draws, and streams from different seeds coalesce (seeds 1 and 3 share 124 of their first 240 draws; seeds 1, 2, 3 give first draws 0.946, 0.947, 0.949). Different synth_seed values therefore produce largely the same forcing modes. Add s_prng_splitmix32, a 32-bit SplitMix-style generator (Weyl sequence + MurmurHash3 finalizer) in exact int64 arithmetic with no intermediate above 2^49, so it is compiler-independent and overflow-free, and use it for the synthetic-turbulence wave vectors and phases. s_prng is left unchanged for its other caller (pre_process mixing-layer perturbation). This changes the realized forcing for every existing synth_seed; the 3D synthetic_turbulence golden (CB853530) must be regenerated. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
Review effort: Lite
Findings: 4
Open (8)
This change alters the realized synthetic-turbulence forcing for existingsynth_seedvalues, so… · New This change alters the realized synthetic-turbulence forcing for existingsynth_seedvalues, so… · New This change alters the realized synthetic-turbulence forcing for existingsynth_seedvalues, so… · New This change alters the realized synthetic-turbulence forcing for existingsynth_seedvalues, so… · New Usinginteger(kind=8)relies on compiler-specific kind numbering. For portability (and to match… · New Usinginteger(kind=8)relies on compiler-specific kind numbering. For portability (and to match… · New The routine returnsvar = real(z, wp)/4294967296._wp, which yields values in [0, 1) (1.0 is not… · New This comment line is very long and may exceed typical free-form Fortran line-length limits used in… · New
What changed in this PR
This PR fixes synthetic-turbulence seeding by replacing the current rounded LCG-based draws with an exact 32-bit integer SplitMix-style PRNG to prevent seed stream collapse and improve ensemble independence while keeping compiler-independent reproducibility.
Changes:
- Switch synthetic-turbulence mode initialization to use
s_prng_splitmix32and a 64-bit integer PRNG state masked to 32 bits. - Add
s_prng_splitmix32(and helperf_mulmod32) tom_helperimplementing exact integer arithmetic with a MurmurHash3-style finalizer.
| File | Description |
|---|---|
| src/simulation/m_body_forces.fpp | Uses the new SplitMix-style PRNG for synthetic turbulence mode vector/phase draws and updates seed handling. |
| src/common/m_helper.fpp | Introduces s_prng_splitmix32 (SplitMix/Weyl + fmix32) and f_mulmod32 for exact 32-bit modular multiplication. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| subroutine s_prng_splitmix32(var, state) | ||
|
|
||
| real(wp), intent(out) :: var | ||
| integer(kind=8), intent(inout) :: state !< in [0, 2^32) |
There was a problem hiding this comment.
Done in 4d6c129. I used selected_int_kind(18), the convention m_helper already uses for 64-bit integers (int64_kind in double_factorial/factorial); src/ does not use iso_fortran_env for this. The literals are now _int64_kind, and the caller in m_body_forces declares seed with the same kind. The generator also reduces the state mod 2^32 itself, so the caller no longer needs a kind-specific mask literal. The output stream is bit-identical to before: I checked against a Python model with gfortran and CCE, including negative seeds.
| !> a*b mod 2^32 for a, b in [0, 2^32), split into 16-bit halves of b so no intermediate exceeds 2^49 | ||
| pure function f_mulmod32(a, b) result(c) | ||
|
|
||
| integer(kind=8), intent(in) :: a, b |
There was a problem hiding this comment.
Done in 4d6c129. I used selected_int_kind(18), the convention m_helper already uses for 64-bit integers (int64_kind in double_factorial/factorial); src/ does not use iso_fortran_env for this. The literals are now _int64_kind, and the caller in m_body_forces declares seed with the same kind. The generator also reduces the state mod 2^32 itself, so the caller no longer needs a kind-specific mask literal. The output stream is bit-identical to before: I checked against a Python model with gfortran and CCE, including negative seeds.
|
|
||
| end function modmul | ||
|
|
||
| !> Uniform draw in [0, 1] from a 32-bit SplitMix-style generator (Weyl sequence hashed by the MurmurHash3 finalizer). Exact |
There was a problem hiding this comment.
Fixed in 4d6c129: the docstring now says [0, 1). One caveat: with wp in single precision, real(2^32-1, sp)/2^32 rounds to exactly 1.0, so the comment says 1 is reachable only through that rounding.
| !> Uniform draw in [0, 1] from a 32-bit SplitMix-style generator (Weyl sequence hashed by the MurmurHash3 finalizer). Exact | ||
| !! integer arithmetic, so the stream is compiler-independent; period 2^32, and different seeds give unrelated streams. |
There was a problem hiding this comment.
I wrapped it in 4d6c129, but ./mfc.sh format (ffmt) reflows comment blocks to fill the repo's 132-column limit, so it rejoins manual wraps. The resulting lines are at most 127 columns, within the limit and like the surrounding code, and the precheck formatting check passes.
The mode draws now come from s_prng_splitmix32, so the realized forcing for synth_seed = 1 changes. Generated with GNU 12.3 (gcc-native/12.3), Debug, no MPI, CPU, matching the configuration recorded in the previous golden (GNU 12.2, Debug, no MPI). Co-Authored-By: Claude <noreply@anthropic.com>
Replace integer(kind=8) and _8 literals with selected_int_kind(18), as m_helper already does for double_factorial/factorial. The seed is now reduced mod 2^32 by the generator itself, so the caller needs no kind-specific literal. The stream is bit-identical to before (gfortran and CCE), so the CB853530 golden is unchanged. Clarify that draws lie in [0, 1). Co-Authored-By: Claude <noreply@anthropic.com>
Lines of Code
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1946 +/- ##
==========================================
+ Coverage 62.64% 62.66% +0.01%
==========================================
Files 86 86
Lines 22425 22435 +10
Branches 3325 3325
==========================================
+ Hits 14048 14058 +10
Misses 6119 6119
Partials 2258 2258 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|



Description
Different
synth_seedvalues produce mostly the same synthetic-turbulence forcing, so seeded ensembles are not independent.Root cause.
s_initialize_body_forces_moduledraws the wave vectors and phases withs_prng/modmul(src/common/m_helper.fpp).modmulrounds the LCG state to 5 decimals every step (decimal_trim = 1e5). That makes the map many-to-one: every seed falls into the same short cycle, and streams from different seeds merge. Here is a Python reproduction ofs_prng(same constants and rounding):The same holds for seeds 4, 5, 10 and 100. In 3D each mode takes 5 draws, so 48 modes use 240 draws:
Seeds tested: 1, 2, 3, 4, 5, 10, 100, 12345.
Observed impact. We ran 3D forced flow past a flapping wing on Frontier with seeds 1, 2 and 3 as an "independent" ensemble. The upstream probe's w(t) correlated at r = 0.44-0.76 between seeds over t = 3-5, and at r = 0.73-0.85 over t = 4-12. Lift during the first wingbeat had a seed-to-seed standard deviation of 0.003, against an excursion of 1.7.
Fix.
s_prng_splitmix32tom_helper: a 32-bit SplitMix-style generator, i.e. a Weyl sequence with step0x9E3779B9hashed by the MurmurHash3fmix32finalizer. It has period 2^32 and is a bijective hash, so there is no cycle collapse.selected_int_kind(18), as elsewhere inm_helper). The 32-bit multiply is split over 16-bit halves (f_mulmod32), so no intermediate exceeds 2^49. There is no signed overflow and no floating-point rounding, so the stream is identical on every compiler. I checked this with a standalone program: gfortran and CCE give bit-identical output to each other and to a Python model.seedbecomes a 64-bit integer initialized fromsynth_seed, which the generator reduces mod 2^32. The generator runs only on rank 0 at init, on the host, so there are no GPU implications.s_prngitself is unchanged. Its only other caller iss_generate_random_perturbationinsrc/pre_process/m_perturbation.fpp(the mixing-layer perturbation, testmixlayer_perturb). That caller reseeds per mode and y-location and takes only 5 draws per seed. Changings_prngwould silently change those initial conditions and goldens, so this PR leaves it alone. The rounding can still map different (k, y) inputs onto correlated draws there. Maintainers may want to switch that caller too, as a separate change.Impact on results / goldens
synth_seed, so runs with synthetic turbulence will not reproduce their pre-PR forcing. Nothing else changes.CB853530(3D -> synthetic_turbulence,synth_seed = 1) is regenerated in this PR (commit 80b58e1). It was generated on a Frontier CPU compute node with GNU Fortran 12.3.0 (gcc-native/12.3) in Debug mode without MPI, via./mfc.sh test --generate --only CB853530 --no-gpu --no-mpi --debug. This matches the configuration recorded in the old golden's metadata (GNU 12.2.0, Debug, no MPI) except for the GCC minor version.tests/CB853530/changed. The new golden has the same 12 entries and sizes as the old one. The t = 0 fields are identical; the step-3 fields differ by at most about 1e-3, the size of the forcing increment itself, and all values are finite../mfc.sh test --only CB853530passes.D13FDB23(3D -> mixlayer_perturb, the others_prnguser) still passes against its unchanged golden.examples/2D_synthetic_turbulenceis already in the skip list incases.py, so its golden is not checked.Verification
s_prngand of the new generator.s_prng_splitmix32in standalone Fortran matches the Python model bit-for-bit (first 3 draws for seeds 1-3, and draw 10^6 for seed 12345) under both gfortran and CCE 19.simulationbuilds on CPU with CCE 19 and with GNU 12.3. Apart from the 3-step regression tests above, I did not run this generator in a simulation. In our production runs we confirmed independence with a local, opt-in workaround that used the intrinsicrandom_number; I am not proposing that here because it is not reproducible across compilers.Contribution Policy
We do not accept pull requests generated primarily by AI without genuine understanding or real-world usage context.
All contributions are expected to demonstrate:
If these expectations are not met, we would prefer to implement the changes ourselves rather than spend time reviewing low-effort submissions.
Acknowledgement
This PR was prepared with the assistance of an AI tool (Claude Code). We found the problem while analyzing a seeded ensemble of production runs on Frontier.
PR template credit: junegunn