We are observing fixed spatial pattern noise in simulations when using the disk source type. Although some starting seed numbers can minimize the observed pattern noise, it is generally pervasive across different seed numbers.
The current RNG implementation appears to use an xorshift128-style RNG to initialize each photon’s RNG state by combining the global seed words with the photon index
ran.reseed(seeds.x ^ i, seeds.y | i, seeds.z ^ i, seeds.w | i);
This implementation of the xorshift128 appears to be causing the fixed pattern noise. Note that pattern/noise generated by xorshift128 has been previously reported.[1]
I was able to eliminate the fixed pattern noise by preconditioning the seed and photon index with SplitMix64 before initializing the xorshift-style RNG state.[2] This approach preserves deterministic behavior for a given seed and photon index while causing adjacent photon indices to produce substantially different initial RNG states.
The attached figure shows top-down reflectance together with two-dimensional sagittal and coronal deposited-energy slices for a 20 × 20 × 20 mm block containing an embedded vessel. It compares the fixed pattern noise observed with the current implementation against the results obtained using SplitMix64-based reseeding.
Example .json that should reproduce the fix-pattern noise in the figure:
channel_block.json
Preliminary timing tests did not show a significant increase in simulation time with this implementation. Below is the code I used to implement the proposed change. It is worth noting that I used OpenAI Codex to assist with writing the code.
Replace the current per-photon reseeding line with:
MCX_reseed_photon(ran, seeds, i);
where
inline uint64_t MCX_splitmix64(uint64_t x) {
x += 0x9E3779B97F4A7C15ULL;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
return x ^ (x >> 31);
}
inline void MCX_reseed_photon(MCX_rand& ran, const dim4& seeds, uint64_t photonid) {
uint64_t base0 = ((uint64_t)seeds.x << 32) | seeds.y;
uint64_t base1 = ((uint64_t)seeds.z << 32) | seeds.w;
uint64_t state0 = MCX_splitmix64(base0 ^ photonid);
uint64_t state1 = MCX_splitmix64(base1 ^ (photonid + 0xD1B54A32D192ED03ULL));
if ((state0 | state1) == 0ULL) {
state1 = 0x9E3779B97F4A7C15ULL;
}
ran.reseed((uint32_t)(state0 >> 32), (uint32_t)state0,
(uint32_t)(state1 >> 32), (uint32_t)state1);
}
Citations:
- Haramoto, H., Matsumoto, M., and Saito, M. “Unveiling patterns in xorshift128+ pseudorandom number generators.” Journal of Computational and Applied Mathematics, 2022.
- Steele, G. L. Jr., Lea, D., and Flood, C. H. “Fast Splittable Pseudorandom Number Generators.” OOPSLA, 2014.
We are observing fixed spatial pattern noise in simulations when using the disk source type. Although some starting seed numbers can minimize the observed pattern noise, it is generally pervasive across different seed numbers.
The current RNG implementation appears to use an xorshift128-style RNG to initialize each photon’s RNG state by combining the global seed words with the photon index
ran.reseed(seeds.x ^ i, seeds.y | i, seeds.z ^ i, seeds.w | i);This implementation of the xorshift128 appears to be causing the fixed pattern noise. Note that pattern/noise generated by xorshift128 has been previously reported.[1]
I was able to eliminate the fixed pattern noise by preconditioning the seed and photon index with SplitMix64 before initializing the xorshift-style RNG state.[2] This approach preserves deterministic behavior for a given seed and photon index while causing adjacent photon indices to produce substantially different initial RNG states.
The attached figure shows top-down reflectance together with two-dimensional sagittal and coronal deposited-energy slices for a 20 × 20 × 20 mm block containing an embedded vessel. It compares the fixed pattern noise observed with the current implementation against the results obtained using SplitMix64-based reseeding.
Example .json that should reproduce the fix-pattern noise in the figure:
channel_block.json
Preliminary timing tests did not show a significant increase in simulation time with this implementation. Below is the code I used to implement the proposed change. It is worth noting that I used OpenAI Codex to assist with writing the code.
Replace the current per-photon reseeding line with:
MCX_reseed_photon(ran, seeds, i);where
Citations: