Skip to main content
Stack Overflow
  1. About
  2. For Teams

-COMPLETE- Code Challenge #9: Random Number Generator

Created
Active
Viewed 3k times
23 entries
20

Update: October 6, 2025: Thanks to everyone who participated in this challenge! It was very interesting to see the diverse approaches to random number generation.

Congratulations to the following users for completing the challenge: Nevpzo, Rohit kumar, MNBLabs, Nanigashi, Herry Poter, Hmwat, Syedali Rizvi, MUGWANEZA MANZI Audace, JGK, Tomas Langkaas, Jérôme Migné, Tomodovodoo, DannyNiu

Original Challenge post

Today's challenge is about random numbers. Programming languages usually contain functions to generate random numbers, but in this challenge, we will create our own. And then, we'll measure how random the output really is.

A good approach to generating pseudo-random numbers is a described in this Stack Overflow answer. It uses a linear congruential generator (LCG) algorithm.

The Challenge

Create a Random Number Generator (RNG) using the LCG algorithm (or another of your choice).

For these algorithms, we need some starting inputs to create randomness. You can use your creativity to find the inputs which are somewhat non-deterministic. Some options could be:

  • user input based, like the time between keystrokes, mouse movement or similar

  • time based, like the time required to do something like ping a server, write a file, etc.

  • other ideas, per your creativity

Using this RNG, generate 100,000 random integers with values between 0-100.

Then test the distribution for randomness in two ways:

  • Shannon Entropy test: This test measures the distribution of the results, i.e., are values uniformly distributed among the possible set of values. An example of how to implement this test is in this Stack Overflow answer. (Follow the discrete distribution approach.) For random numbers from 0-100, the lowest entropy would be 0 and the highest entropy would be around 6.6 bits.

  • Data compressibility test: Testing for uniform distribution is not a sufficient criteria for randomness. If the RNG produced an output of [0, 1, 2, 3 ...100, 0, 1, 2...] it would have a uniform distribution, but it wouldn't be very random. One way to check for this is by looking at how well the data compresses. Truly random data has no predictable patterns, so it is hard to compress. Data with sequential or repeating patterns compresses much more, revealing that the sequence isn’t truly random. To do this test, compress the data using a DEFLATE algorithm available in most programming languages, and compute the ratio of the size of the compressed and uncompressed data. Ideally we get a ratio close to 1.0, meaning that the data could not be compressed.

    Key dates

    You have two weeks from the date this challenge is posted to submit your entry.

    September 19: Challenge goes live

    October 3: Challenge is complete and we'll announce winners soon after. We plan to recognize all participants that complete the challenge.

    While we will be recognizing all of the complete entries, we still encourage participants to vote on the responses. We recommend upvoting solutions that have good randomness metrics and/or creative approaches to the problem.

    How to Submit

    Enter your submission in the text box below.

    Your submission should include:

    • A description of how you created the random numbers and your results on the entropy and data compression tests

    • The code you used to create the random numbers

    • [Optional] Anything you learned or any interesting challenges you faced while coding

    Your entry is not permitted to be written by AI. For any feedback on this Challenge, please head over to the Meta post.

23 entries
Sorted by:
79790494
0

for num in numbers

add 1 to number

 for num2 in numbers
 divide num by num2, add 5 * 6**12円**13円 * 3.5
 for num3 in numbers
 num1 * num2 / num2 % num1 = num3
 num3 ++
79790233
-1
import time, os, zlib, math, statistics
from collections import Counter
class LCG:
 MOD = 1 << 48
 A = 25214903917
 C = 11
 def __init__(self, seed: int):
 self.state = seed & (self.MOD - 1)
 @staticmethod
 def _mix_seed():
 t1 = time.time_ns()
 t2 = int(time.perf_counter_ns())
 pid = os.getpid()
 acc = 0
 for i in range(1000):
 t = time.perf_counter_ns()
 acc ^= (t << (i % 13)) & ((1 << 64) - 1)
 seed = t1 ^ (t2 << 1) ^ (pid << 17) ^ acc
 x = (seed + 0x9E3779B97F4A7C15) & ((1 << 64) - 1)
 x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9 & ((1 << 64) - 1)
 x = (x ^ (x >> 27)) * 0x94D049BB133111EB & ((1 << 64) - 1)
 x = x ^ (x >> 31)
 return x
 @classmethod
 def with_mixed_seed(cls):
 return cls(cls._mix_seed())
 def next(self):
 self.state = (self.A * self.state + self.C) % self.MOD
 return self.state
 def next_int_0_100(self):
 s = self.next()
 value = (s >> 16) & 0xFFFFFFFF
 return value % 101
def main():
 rng = LCG.with_mixed_seed()
 N = 100_000
 nums = [rng.next_int_0_100() for _ in range(N)]
 cnt = Counter(nums)
 H = 0.0
 for k in range(101):
 p = cnt.get(k, 0) / N
 if p > 0:
 H -= p * math.log2(p)
 raw = bytes(nums)
 comp = zlib.compress(raw, level=9)
 ratio = len(comp) / len(raw)
 mean = sum(nums) / N
 var = statistics.pvariance(nums)
 expected_variance = 100 * 102 / 12
 print(f"N = {N}")
 print(f"Valores únicos: {len(cnt)} (esperado: 101)")
 print(f"Entropia (bits): {H:.6f} (máx teórico = {math.log2(101):.6f})")
 print(f"Tamanho bruto: {len(raw)} bytes; comprimido: {len(comp)} bytes")
 print(f"Razão comprimido/bruto: {ratio:.5f}")
 print(f"Média: {mean:.5f} (esperado ≈ 50)")
 print(f"Variância: {var:.3f} (esperado ≈ {expected_variance:.3f})")
 print(f"Mín/Máx: {min(nums)} / {max(nums)}")
if __name__ == "__main__":
 main()
79789819
0

Random Number Generation

A Linear Congruential Generator (LCG) was implemented in Python. The LCG uses the following formula:

X_n+1 = (a * X_n + c) mod m

Where:

  • •X_n is the current pseudo-random number.

  • •a is the multiplier.

  • •c is the increment.

  • •m is the modulus.

  • •seed is the starting value (X_0).

For this implementation, the following parameters were used:

  • •seed = 12345

  • •a = 1103515245

  • •c = 12345

  • •m = 2^32

100,000 random integers were generated with values between 0 and 100 (inclusive).


import math
import zlib
class LCG:
 def __init__(self, seed, a, c, m):
 self.seed = seed
 self.a = a
 self.c = c
 self.m = m
 def next(self):
 self.seed = (self.a * self.seed + self.c) % self.m
 return self.seed
def generate_numbers(rng, count, min_val, max_val):
 numbers = []
 for _ in range(count):
 # Scale the LCG output to the desired range [min_val, max_val]
 scaled_value = min_val + (rng.next() / rng.m) * (max_val - min_val + 1)
 numbers.append(int(scaled_value))
 return numbers
def shannon_entropy(data, min_val, max_val):
 if not data:
 return 0
 # Calculate frequency of each number
 counts = [0] * (max_val - min_val + 1)
 for x in data:
 if min_val <= x <= max_val:
 counts[x - min_val] += 1
 # Calculate probabilities and entropy
 entropy = 0.0
 total_count = len(data)
 for count in counts:
 if count > 0:
 probability = count / total_count
 entropy -= probability * math.log2(probability)
 return entropy
def data_compressibility(data):
 # Convert list of integers to a byte string for compression
 # A simple way is to join them with a separator and encode
 data_str = ",".join(map(str, data))
 original_size = len(data_str.encode("utf-8"))
 compressed_data = zlib.compress(data_str.encode("utf-8"), level=9)
 compressed_size = len(compressed_data)
 if original_size == 0:
 return 0 # Avoid division by zero
 return compressed_size / original_size
if __name__ == "__main__":
 # LCG parameters (often chosen to be large primes or powers of 2)
 # These are common values, but can be adjusted
 seed = 12345 # A starting seed
 a = 1103515245
 c = 12345
 m = 2**32 # Modulus
 rng = LCG(seed, a, c, m)
 num_count = 100000
 min_val = 0
 max_val = 100
 print(f"Generating {num_count} random numbers between {min_val} and {max_val}...")
 random_numbers = generate_numbers(rng, num_count, min_val, max_val)
 print("Numbers generated.")
 print("\nPerforming Shannon Entropy test...")
 entropy = shannon_entropy(random_numbers, min_val, max_val)
 print(f"Shannon Entropy: {entropy:.4f} bits")
 print(f"Expected max entropy for {max_val - min_val + 1} values: {math.log2(max_val - min_val + 1):.4f} bits")
 print("\nPerforming Data Compressibility test...")
 compress_ratio = data_compressibility(random_numbers)
 print(f"Data Compressibility Ratio (compressed size / original size): {compress_ratio:.4f}")
 print("Ideally, this ratio should be close to 1.0 for truly random data.")
 # Optional: Print a few numbers to inspect
 # print("\nFirst 10 generated numbers:", random_numbers[:10])
 # Optional: Save numbers to a file for further inspection
 # with open("random_numbers.txt", "w") as f:
 # for num in random_numbers:
 # f.write(str(num) + "\n")
79782839
1

A simple 64-bit permutation in a sponge construction.

#include <stdio.h>
#include <stdint.h>
#include <time.h>
#include <math.h>
uint64_t Permute(uint64_t x)
{
 for(int r=0; r<10; r++) // 10 rounds.
 {
 x += 0x0102030405060708;
 x = (x << 17) | (x >> 47);
 x ^= 0x020305070b0d1113;
 x *= 0x0807060504030201;
 }
 return x;
}
static uint64_t state = 0;
void srand(uint32_t seed)
{
 state ^= seed;
 state = Permute(state);
}
uint32_t rand()
{
 return (state = Permute(state)) % 100;
}
int main(int argc, char *argv[])
{
 int stats[100] = {};
 double e = 0;
 FILE *fp = fopen(argv[1], "wb");
 srand(time(NULL)); // Could be any other source, time, pid, etc. The stats were collected with the seed number 123.
 for(long i=0; i<100000; i++) 
 {
 uint8_t c = rand();
 fputc(c, fp);
 stats[c] ++;
 }
 for(int i=0; i<100; i++)
 {
 e += stats[i] * log2(stats[i] / 100000.0) / 100000.0;
 }
 printf("Shannon Entropy: %f\n", -e);
 return 0;
}

Saving this source code as a C source code with filename randch.c, and execute the following on a POSIX command line, and you get the following interaction:

$ make randch && time ./randch randblob
make: `randch' is up to date.
Shannon Entropy: 6.643187
real 0m0.012s
user 0m0.009s
sys 0m0.002s
$ gzip -9 < randblob > rand.gz && ls -l rand.gz
-rw-r--r-- 1 dannyniu staff 84103 Oct 5 15:10 rand.gz
$

Note that you need a toolchain supporting at least C99 for the code to work.

The take away is that, a cryptographic PRNG is sufficient for practically all, including scientific purposes, barring requirement of unrealistically long periods. My related post: https://cs.stackexchange.com/questions/160133/the-awkward-status-of-mersenne-twister

Given that efficiency of most cryptographic algorithms, I believe that I might be the best performing entrant here. I noticed that someone used SHA-256, that's a brilliant idea, and maybe they'll get better on systems with native CPU intrinsics, such as ARMv8, AMD x86 some low-powered Intel chips.

79782901
0
  • 15.3k
  • 5
  • 28
  • 44

The output generated does not seem to be random? Or is this because you use: srand(123) ? But that does implies you did not read, or understand: "Programming languages usually contain functions to generate random numbers, but in this challenge, we will create our own" (IMHO)

79782942
0

@Luuk, Yeah, my submission is a PRNG (i.e. Pseudo-Random Number Generator), I put 123 there for reproducibility. For true randomness (i.e. a TRNG) you need an HSM (hardware security module) or CPU instructions such as RDRAND for that. All that we have at hand, are deterministic sources such as time, or process identifiers. Edited my submission to use time() function this time.

79782676
0
# Example using Folium library
import folium
map = folium.Map(location=[37.7749, -122.4194], zoom_start=10)
for location in target_locations:
 folium.Marker(
 location=[location['lat'], location['lng']],
 popup=location['info']
 ).add_to(map)
 
map.save("phone_recon_map.html")
79781680
2

Trinity Chaotic RNG - Extracting Randomness from Mathematical Beauty

When it comes to randomness, numbers like π, Euler's number, and prime sequences have always fascinated me. These constants appear chaotic and unpredictable despite being completely deterministic. I wondered: could I harness this mathematical "chaos" to build a truly random number generator?

Instead of using a traditional LCG approach, I decided to create something more ambitious - a multi-layered chaotic system that combines three independent sources of mathematical entropy. I call it the 😈 Trinity Chaotic Engine.

The Approach

My RNG combines three distinct chaotic layers:

Layer 1: Irrational Walker - Dynamically walks through the digits of seven transcendental constants (π, e, φ, √2, √3, ln(2), Catalan's constant). The digits themselves determine which constant to jump to next, creating unpredictable paths through mathematical space.

Layer 2: Complex Chaos - Uses bounded complex number dynamics with trigonometric functions to maintain chaotic behavior without overflow. Extracts entropy from both magnitude and phase components.

Layer 3: Prime Mixer - Indexes into multiple constants using prime numbers, then applies non-linear mixing with feedback loops.

All three layers feed into a SHA-256 avalanche mixer, ensuring that tiny changes in any layer cause massive changes in the output.

Entropy Harvesting: The seed combines microsecond-precision system time with memory addresses, hashed through SHA-256 for cryptographic strength.

Test Results (100,000 numbers, range 0-100)

Shannon Entropy: 6.657456 bits
Maximum Possible: 6.658211 bits
Achieved: 99.99% of maximum ✓
Compression Ratio: 0.8424
(Data compressed to 84.24% of original) ✓
Chi-Square: 104.32
Distribution: Excellent uniformity ✓
Generation Rate: ~133,000 numbers/second

The entropy score is essentially perfect - achieving 99.99% of the theoretical maximum for this range. The chi-square test confirms excellent distribution uniformity across all 101 possible values.

trinity_chaotic_rng.py

import mpmath
import cmath
import time
import hashlib
import zlib
import math
import argparse
import sys
from collections import Counter
class TrinityChaoticRNG:
 
 def __init__(self, verbose=False, precision=50000):
 self.verbose = verbose
 mpmath.mp.dps = precision
 
 if verbose:
 print(f"Computing {precision:,} digits of mathematical constants...")
 
 self.constants = {
 'pi': str(mpmath.pi)[2:],
 'e': str(mpmath.e)[2:],
 'phi': str(mpmath.phi)[2:],
 'sqrt2': str(mpmath.sqrt(2))[2:],
 'sqrt3': str(mpmath.sqrt(3))[2:],
 'ln2': str(mpmath.log(2))[2:],
 'catalan': str(mpmath.catalan)[2:],
 }
 
 self.constant_names = list(self.constants.keys())
 
 # Harvest entropy from time and memory
 time_entropy = int(time.time() * 1_000_000)
 memory_entropy = id(object()) ^ id([]) ^ id({})
 entropy_mix = hashlib.sha256(f"{time_entropy}{memory_entropy}".encode()).digest()
 seed = int.from_bytes(entropy_mix[:8], 'big')
 
 # Initialize Layer 1: Irrational Walker
 self.walker_position = seed % (precision // 2)
 self.walker_constant = seed % len(self.constant_names)
 self.walker_step_size = (seed >> 16) % 1000 + 1
 
 # Initialize Layer 2: Complex Chaos
 self.complex_z = complex(
 ((seed & 0xFFFF) / 65536.0) * 0.5,
 (((seed >> 16) & 0xFFFF) / 65536.0) * 0.5
 )
 self.chaos_a = 2.718281828459045
 self.chaos_b = 3.141592653589793
 self.chaos_c = 1.618033988749895
 
 # Initialize Layer 3: Prime Mixer
 self.primes = self._generate_primes(10000)
 self.prime_idx = seed % len(self.primes)
 
 self.entropy_buffer = []
 self.buffer_size = 16
 self.generated_count = 0
 
 if verbose:
 print(f"Initialized with seed: {seed}")
 print("Ready to generate random numbers.\n")
 
 def _generate_primes(self, limit):
 sieve = [True] * limit
 sieve[0] = sieve[1] = False
 for i in range(2, int(limit**0.5) + 1):
 if sieve[i]:
 for j in range(i*i, limit, i):
 sieve[j] = False
 return [i for i, is_prime in enumerate(sieve) if is_prime]
 
 def _layer1_irrational_walker(self):
 constant_name = self.constant_names[self.walker_constant]
 digits_string = self.constants[constant_name]
 digit = int(digits_string[self.walker_position % len(digits_string)])
 
 if digit < 2:
 self.walker_constant = (self.walker_constant + 1) % len(self.constant_names)
 elif digit > 7:
 self.walker_constant = (self.walker_constant - 1) % len(self.constant_names)
 
 step = (digit * 17 + 23) * self.walker_step_size % 997
 self.walker_position = (self.walker_position + step) % len(digits_string)
 self.walker_step_size = (self.walker_step_size + digit) % 1000 + 1
 
 return digit
 
 def _layer2_complex_chaos(self):
 self.complex_z = complex(
 math.sin(self.complex_z.real * self.chaos_b) * self.chaos_a,
 math.cos(self.complex_z.imag * self.chaos_a) * self.chaos_b
 )
 
 magnitude = abs(self.complex_z) * 10000
 phase = cmath.phase(self.complex_z) * 10000
 chaos_value = (int(magnitude) ^ int(phase)) & 0xFFFFFFFF
 
 self.complex_z += complex(0.001 * self.chaos_c, 0.001)
 
 if abs(self.complex_z) > 10:
 self.complex_z = complex(
 self.complex_z.real / abs(self.complex_z),
 self.complex_z.imag / abs(self.complex_z)
 )
 
 return chaos_value
 
 def _layer3_prime_mixer(self):
 prime = self.primes[self.prime_idx]
 
 pi_digit = int(self.constants['pi'][prime % len(self.constants['pi'])])
 e_digit = int(self.constants['e'][(prime * 3) % len(self.constants['e'])])
 phi_digit = int(self.constants['phi'][(prime * 7) % len(self.constants['phi'])])
 
 mixed = (pi_digit * e_digit + phi_digit) ^ (pi_digit + e_digit + phi_digit)
 self.prime_idx = (self.prime_idx + mixed + 1) % len(self.primes)
 
 return mixed
 
 def _avalanche_mix(self, layer1, layer2, layer3):
 combined = (layer1 << 24) | (layer2 & 0xFFFFFF) | (layer3 << 8)
 
 if len(self.entropy_buffer) > 0:
 buffer_xor = 0
 for i, val in enumerate(self.entropy_buffer):
 buffer_xor ^= (val << (i % 32))
 combined ^= buffer_xor
 
 hash_input = f"{combined}{layer1}{layer2}{layer3}{self.generated_count}".encode()
 hash_output = hashlib.sha256(hash_input).digest()
 final_value = int.from_bytes(hash_output[:8], 'big')
 
 self.entropy_buffer.append(final_value & 0xFFFFFFFF)
 if len(self.entropy_buffer) > self.buffer_size:
 self.entropy_buffer.pop(0)
 
 return final_value
 
 def next(self):
 layer1_output = self._layer1_irrational_walker()
 layer2_output = self._layer2_complex_chaos()
 layer3_output = self._layer3_prime_mixer()
 
 final = self._avalanche_mix(layer1_output, layer2_output, layer3_output)
 self.generated_count += 1
 
 return final
 
 def randint(self, a, b):
 raw = self.next()
 return a + (raw % (b - a + 1))
 
 def random(self):
 raw = self.next()
 return (raw & 0xFFFFFFFFFFFF) / 0x10000000000000
 
 def choice(self, sequence):
 return sequence[self.randint(0, len(sequence) - 1)]
 
 def randint_with_length(self, length, min_val, max_val, omit_digits=None):
 max_attempts = 10000
 
 for _ in range(max_attempts):
 num = self.randint(min_val, max_val)
 
 if len(str(num)) != length:
 continue
 
 if omit_digits:
 num_str = str(num)
 if any(d in num_str for d in omit_digits):
 continue
 
 return num
 
 return None
def parse_bracket_arg(arg_value):
 if not arg_value:
 return None
 
 arg_value = arg_value.strip('[]')
 if ',' in arg_value:
 return [item.strip() for item in arg_value.split(',')]
 return arg_value
def validate_cli_args(gen, length, ran, omit):
 errors = []
 warnings = []
 
 if gen is not None:
 try:
 gen = int(gen)
 if gen <= 0:
 errors.append("Generation count must be positive")
 elif gen > 1000000:
 warnings.append(f"Generating {gen} numbers may take a while")
 except ValueError:
 errors.append("Generation count must be a valid integer")
 
 if ran is not None:
 if len(ran) != 2:
 errors.append("Range must have exactly 2 values: [min,max]")
 else:
 try:
 min_val, max_val = int(ran[0]), int(ran[1])
 if min_val >= max_val:
 errors.append("Range minimum must be less than maximum")
 ran = (min_val, max_val)
 except ValueError:
 errors.append("Range values must be valid integers")
 
 if length is not None:
 try:
 length = int(length)
 if length <= 0:
 errors.append("Length must be positive")
 elif length > 18:
 errors.append("Length cannot exceed 18 digits (integer overflow risk)")
 except ValueError:
 errors.append("Length must be a valid integer")
 
 if omit is not None:
 try:
 omit_digits = set(omit)
 if len(omit_digits) > 8:
 errors.append("Cannot omit more than 8 digits (must leave at least 2 for randomness)")
 
 if not all(d in '0123456789' for d in omit_digits):
 errors.append("Omit values must be single digits (0-9)")
 
 except Exception:
 errors.append("Invalid omit format")
 else:
 omit_digits = set()
 
 if length is not None and ran is not None and not errors:
 min_val, max_val = ran
 
 min_possible = 10 ** (length - 1) if length > 1 else 0
 max_possible = (10 ** length) - 1
 
 if max_val < min_possible:
 errors.append(f"Range maximum ({max_val}) is too small for {length}-digit numbers (min: {min_possible})")
 
 if min_val > max_possible:
 errors.append(f"Range minimum ({min_val}) is too large for {length}-digit numbers (max: {max_possible})")
 
 range_size = max_val - min_val + 1
 if range_size < gen * 2:
 warnings.append(f"Range ({range_size} values) is small for {gen} generations. May have duplicates.")
 
 if omit is not None and ran is not None and length is not None and not errors:
 min_val, max_val = ran
 
 available_digits = set('0123456789') - omit_digits
 
 if len(available_digits) < 2:
 errors.append("Must have at least 2 available digits after omission")
 
 if len(available_digits) <= 3:
 warnings.append("Very few digits available. Generation may be slow or impossible.")
 
 if ran is not None and gen is not None and not errors:
 min_val, max_val = ran
 range_size = max_val - min_val + 1
 
 if range_size <= 2 and gen > 2:
 warnings.append(f"Range has only {range_size} possible values for {gen} generations.")
 warnings.append("Consider using floating-point numbers for more variety.")
 
 return errors, warnings, gen, length, ran, omit_digits if omit else None
def cli_generate(gen, length, ran, omit):
 print("Trinity Chaotic RNG - CLI Mode")
 print("=" * 60)
 
 errors, warnings, gen, length, ran, omit_digits = validate_cli_args(gen, length, ran, omit)
 
 if errors:
 print("\nERRORS:")
 for error in errors:
 print(f" ✗ {error}")
 print("\nGeneration aborted.")
 return
 
 if warnings:
 print("\nWARNINGS:")
 for warning in warnings:
 print(f" ⚠ {warning}")
 print()
 
 print("Initializing RNG...")
 rng = TrinityChaoticRNG(verbose=False)
 
 if gen is None:
 gen = 1
 if ran is None:
 ran = (0, 100)
 
 min_val, max_val = ran
 use_floats = False
 
 range_size = max_val - min_val + 1
 if length is None and range_size <= 2 and gen > 2:
 use_floats = True
 print(f"\nGenerating {gen} random floats in range [{min_val}, {max_val}]...")
 elif length is not None:
 print(f"\nGenerating {gen} random {length}-digit numbers in range [{min_val}, {max_val}]...")
 if omit_digits:
 print(f"Omitting digits: {sorted(omit_digits)}")
 else:
 print(f"\nGenerating {gen} random integers in range [{min_val}, {max_val}]...")
 if omit_digits:
 print(f"Omitting digits: {sorted(omit_digits)}")
 
 print()
 
 results = []
 failed_count = 0
 
 for i in range(gen):
 if use_floats:
 float_val = min_val + rng.random() * (max_val - min_val)
 results.append(float_val)
 elif length is not None:
 num = rng.randint_with_length(length, min_val, max_val, omit_digits)
 if num is None:
 failed_count += 1
 print(f" [{i+1}] Failed to generate valid number (constraints too tight)")
 else:
 results.append(num)
 else:
 attempts = 0
 max_attempts = 10000
 
 while attempts < max_attempts:
 num = rng.randint(min_val, max_val)
 
 if omit_digits and any(d in str(num) for d in omit_digits):
 attempts += 1
 continue
 
 results.append(num)
 break
 
 if attempts == max_attempts:
 failed_count += 1
 print(f" [{i+1}] Failed to generate valid number")
 
 print("RESULTS:")
 print("-" * 60)
 for i, num in enumerate(results, 1):
 if use_floats:
 print(f" [{i}] {num:.6f}")
 else:
 print(f" [{i}] {num}")
 
 if failed_count > 0:
 print(f"\n⚠ {failed_count} generation(s) failed due to constraints")
 
 print("=" * 60)
def shannon_entropy(data):
 counter = Counter(data)
 total = len(data)
 entropy = 0.0
 
 for count in counter.values():
 p = count / total
 if p > 0:
 entropy -= p * math.log2(p)
 
 return entropy
def compression_ratio_test(data):
 byte_data = bytes(data)
 compressed = zlib.compress(byte_data, level=9)
 return len(compressed) / len(byte_data), len(byte_data), len(compressed)
def distribution_analysis(data, num_bins):
 counter = Counter(data)
 expected = len(data) / num_bins
 
 chi_square = sum(
 ((counter.get(i, 0) - expected) ** 2) / expected
 for i in range(num_bins)
 )
 
 return counter, chi_square
def run_challenge_tests():
 print("=" * 70)
 print("✨ Trinity Chaotic RNG - Challenge Test Results")
 print("=" * 70)
 print()
 
 print("⏳ Initializing RNG...")
 rng = TrinityChaoticRNG(verbose=False)
 print()
 
 print("Generating 100,000 random integers in range [0, 100]...")
 start_time = time.time()
 numbers = [rng.randint(0, 100) for _ in range(100000)]
 generation_time = time.time() - start_time
 
 print(f"Generated in {generation_time:.2f} seconds")
 print(f"Rate: {100000/generation_time:.0f} numbers/second")
 print()
 
 # Test 1: Shannon Entropy
 print("-" * 70)
 print("🧪 TEST 1: Shannon Entropy")
 print("-" * 70)
 entropy = shannon_entropy(numbers)
 max_entropy = math.log2(101)
 print(f"Shannon Entropy: {entropy:.6f} bits")
 print(f"Maximum Possible: {max_entropy:.6f} bits")
 print(f"Percentage: {(entropy/max_entropy)*100:.2f}%")
 print(f"Result: {'PASS - Excellent randomness' if entropy > 6.5 else 'PASS'}")
 print()
 
 # Test 2: Compression
 print("-" * 70)
 print("🧪 TEST 2: Data Compressibility (DEFLATE)")
 print("-" * 70)
 ratio, original, compressed = compression_ratio_test(numbers)
 print(f"Original size: {original:,} bytes")
 print(f"Compressed size: {compressed:,} bytes")
 print(f"Compression ratio: {ratio:.6f}")
 print(f"Result: Data compressed to {ratio*100:.2f}% of original")
 print()
 
 # Test 3: Distribution
 print("-" * 70)
 print("🧪 TEST 3: Distribution Uniformity (Chi-Square Test)")
 print("-" * 70)
 counter, chi_square = distribution_analysis(numbers, 101)
 expected = 100000 / 101
 print(f"Chi-square statistic: {chi_square:.2f}")
 print(f"Expected count per bin: {expected:.2f}")
 min_count = min(counter.values())
 max_count = max(counter.values())
 print(f"Min/Max occurrences: {min_count} / {max_count}")
 print(f"Result: {'PASS - Excellent uniformity' if chi_square < 130 else 'PASS'}")
 print()
 
 # Summary
 print("=" * 70)
 print("📋 SUMMARY")
 print("=" * 70)
 print(f"Shannon Entropy: {entropy:.4f} bits ({(entropy/max_entropy)*100:.2f}%)")
 print(f"Compression Ratio: {ratio:.4f}")
 print(f"Chi-Square: {chi_square:.2f}")
 print(f"\n✅ All tests passed successfully!")
 print("=" * 70)
 
 return rng, numbers
def demo_usage():
 print("=" * 70)
 print("Trinity Chaotic RNG - Usage Examples")
 print("=" * 70)
 print()
 
 rng = TrinityChaoticRNG(verbose=False)
 print("RNG initialized.\n")
 
 print("Example 1: Generate random integers in range [1, 100]")
 print(" ", [rng.randint(1, 100) for _ in range(10)])
 print()
 
 print("Example 2: Generate random floats in range [0.0, 1.0)")
 for i in range(5):
 print(f" {rng.random():.6f}")
 print()
 
 print("Example 3: Random choice from a list")
 colors = ['red', 'green', 'blue', 'yellow', 'purple']
 for i in range(5):
 print(f" {rng.choice(colors)}")
 print()
 
 print("Example 4: Roll a six-sided die 10 times")
 rolls = [rng.randint(1, 6) for _ in range(10)]
 print(f" {rolls}")
 print()
 
 print("=" * 70)
if __name__ == "__main__":
 parser = argparse.ArgumentParser(
 description='Trinity Chaotic Random Number Generator',
 epilog='Example: python trinity_chaotic_rng.py --gen 4 --len 2 --ran 40,100 --omit 2,5'
 )
 
 parser.add_argument('--gen', type=str, help='Number of random numbers to generate (e.g., --gen 4)')
 parser.add_argument('--len', type=str, help='Length of each number in digits (e.g., --len 2)')
 parser.add_argument('--ran', type=str, help='Range as min,max (e.g., --ran 40,100)')
 parser.add_argument('--omit', type=str, help='Digits to omit (e.g., --omit 2,5)')
 parser.add_argument('--test', action='store_true', help='Run challenge tests')
 parser.add_argument('--demo', action='store_true', help='Show usage demo')
 
 args = parser.parse_args()
 
 if args.gen or args.len or args.ran or args.omit:
 gen = parse_bracket_arg(args.gen)
 length = parse_bracket_arg(args.len)
 ran = parse_bracket_arg(args.ran) if args.ran else None
 omit = list(args.omit.replace(',', '')) if args.omit else None
 
 cli_generate(gen, length, ran, omit)
 elif args.test:
 rng, numbers = run_challenge_tests()
 elif args.demo:
 demo_usage()
 else:
 rng, numbers = run_challenge_tests()
 print()
 demo_usage()

CLI Usage

# Run default tests
python trinity_chaotic_rng.py
# Generate 4 random 2-digit numbers between 40-100, excluding digits 2 and 5
python trinity_chaotic_rng.py --gen 4 --len 2 --ran 40,100 --omit 2,5
# Generate 10 random numbers in a specific range
python trinity_chaotic_rng.py --gen 10 --ran 0,100
# Generate with length constraints
python trinity_chaotic_rng.py --gen 5 --len 3 --ran 100,999

Example output:

Trinity Chaotic RNG - CLI Mode
============================================================
Initializing RNG...
Generating 4 random 2-digit numbers in range [40, 100]...
Omitting digits: ['2', '5']
RESULTS:
------------------------------------------------------------
 [1] 87
 [2] 96
 [3] 41
 [4] 78
============================================================

The CLI includes professional edge case handling - it automatically detects contradicting constraints (like requiring 5-digit numbers in range ) and switches to floating-point generation when the range is too small.

The Code

Full implementation: GitHub - Trinity Chaotic Engine

What I Learned

The biggest challenge was keeping the complex chaos layer bounded - it kept overflowing until I switched to trigonometric functions. I also discovered that combining multiple entropy sources through XOR and cryptographic hashing creates much stronger randomness than any single source.

The most interesting insight? Mathematical constants that seem "random" actually have excellent statistical properties when you extract them chaotically. The digits of π alone wouldn't be enough, but by jumping between multiple constants based on the digits themselves, you create genuine unpredictability.

And building the CLI taught me the importance of edge case handling - there are so many ways constraints can contradict each other (like asking for 5-digit numbers in range ). The validator catches these and provides helpful error messages instead of just failing.

79782566
1

I like the inclusion of generation rate as a metric! Smart to consider performance with this operation.

79782642
0

@setman Thanks! From my experiences I figured performance matters when generating large datasets. The multi-layer approach is computationally heavier than a simple LCG, so measuring throughput helps assess the practical trade-off between randomness quality and speed.

79781429
1

Hey, this is my first post so hopefully I'm doing this right!

I decided to use a similar method to older taught randomness methods from high school, choosing a random digit from pi. Additionally, I wanted to minimally use external python packages, and didn't care TOO much about speed, and did NOT want to use pi. It's overrated anyway, only used for circles and signals and like everything somehow.

However, I did not want to use a seed, and my mind wondered off to $i^i$. Though, taking into account precision, this probably wouldn't work great, and generating a big number just didn't sound interesting. So how about I take the nth tertrate of i, and take m chunks? Then, per additional tetration I can take m chunks again, and I can analyse if the limit of tertration of i has a (semi-)uniform distribution or not!

I had 2 issues, my digits would range from 0-999, and I wasn't sure if the tertrate of i was actually trancendental (or real!, or converging(??)).

For my first point, this was quickly solved by taking blocks of 4 digits instead of 3, and performing a mod 101 operation, discarding only the "0000" digit. It seems for smaller tetrations the zeroes expand quite frequently during our math, so even though all ABAB-like numbers should be able to be rejected (since for 0000-9999, mod 101, we expect 100 on == 0, and 99 on any other mod, so any mod101 == 0 number should be rejected), I found that a series of zeroes was persistent throughout the distribution. BE GONE, that will be our ABAB rejection completed!

Then, for the second part, I actually couldn't find out that the tertrate of i is proven to be trancendental, though, I went into the rabbit hole, the tertrate of i converges to a specific value! And the principal root (im using), when normalized, converges to pi/2 in some manner. IM JUST USING PI? However, this convergence is quite slow, netting ~0.04987 new correct digits per tertrate depth. I did need to take it into account, and I played quite a bit with which digits I would take to circumvent the convergence. So in the end, I'm taking the distribution of numbers of the converging tetrate of i, reading a 4-long chunk, mod 101 to get our 101 different numbers, discard 0000 because it was messing with our results, and then... These were my results!

Entropy: 6.657523, # ideal entropy == 6.658211
DEFLATE ratio: 0.842630 # perfect == 1
DEFLATE (Byte-packed): 1.000432 #It seems some headers even increase our "compressed" size!
Chi-square: 95.40

Definitely not optimal, it's python and I'm using strings here and there, but overall I'm quite happy with my small project! To be honest, for a random numer generator, I'm impressed! I didn't expect I'd make it this far. I will hopefully never have to read or try to understand Lambert-W functions ever again. My biggest fear.
Additionally, who knew python had a limit of 4300 digits for integer string conversion? Not me, before this...

EDIT: I realized that for a lot of people, their DEFLATE was looking a little... off. about ~0.16666 off. like. like 6->5 bytes for people who did proper packing, and who did get a 1 on their DEFLATE.
Time to edit my code! Admittedly, I had to search through quite a bit of stackoverflow to find how to, but This thread helped quite a bit. I ended up being able to successfully compress my numbers to 5 bytes, and raising my DEFLATE to above 1, somehow.

I added my barely commented code [Now with the corrected DEFLATE!].

import math
import time
import zlib
from collections import Counter
import mpmath as mp
class Pow10Cache:
 __slots__ = ("_c",)
 def __init__(self):
 self._c = {0: mp.mpf(1)}
 def get(self, k: int) -> mp.mpf:
 c = self._c
 if k in c:
 return c[k]
 # compute iteratively from nearest lower power if available
 if (k-1) in c:
 c[k] = c[k-1] * 10
 return c[k]
 c[k] = mp.power(10, k)
 return c[k]
_P10 = Pow10Cache()
def fractional_digits_slice_abs(x: mp.mpf, start: int, length: int) -> str:
 if length <= 0:
 return ""
 fabs, floor = mp.fabs, mp.floor
 x = fabs(x)
 frac = x - floor(x)
 k = start + length
 scaled = floor(frac * _P10.get(k))
 i_scaled = int(scaled)
 mod = int(_P10.get(length))
 tail = i_scaled % mod
 return f"{tail:0{length}d}"
def stream_after_skip(
 re_val: mp.mpf,
 im_val: mp.mpf,
 D_frac: int,
 skip: int,
 need_digits: int,
 use_both: bool = True,
) -> str:
 if need_digits <= 0:
 return ""
 if use_both:
 total_len = 2 * D_frac
 if skip >= total_len:
 return ""
 need = min(need_digits, total_len - skip)
 out = []
 # portion from Re(|z|)
 if skip < D_frac and need > 0:
 take_re = min(need, D_frac - skip)
 out.append(fractional_digits_slice_abs(re_val, skip, take_re))
 need -= take_re
 skip = 0
 else:
 skip -= D_frac
 # portion from Im(|z|)
 if need > 0:
 take_im = min(need, D_frac - skip)
 if take_im > 0:
 out.append(fractional_digits_slice_abs(im_val, skip, take_im))
 return "".join(out)
 else:
 # 2 * D_frac digits from |Re| only
 total_len = 2 * D_frac
 if skip >= total_len:
 return ""
 need = min(need_digits, total_len - skip)
 return fractional_digits_slice_abs(re_val, skip, need)
# Table of n % 101 for n in [0..9999]. Tested this shit with Pi and Range, is fully accurate.
_MOD101_TABLE = tuple(n % 101 for n in range(10000))
def extract_mod101(stream: str, need: int, block: int = 4):
 out = bytearray()
 i = 0
 L = len(stream)
 accepted = 0
 rejected = 0
 tbl = _MOD101_TABLE
 b = block
 while len(out) < need and i + b <= L:
 s = stream[i:i + b]
 if s == "0000": #Create a 1:1 mapping, only works on 0000. Decimal expansion on large numbers outside of any convergeance makes this inherently bad.
 rejected += 1
 else:
 x = (ord(s[0]) - 48) * 1000 + (ord(s[1]) - 48) * 100 + (ord(s[2]) - 48) * 10 + (ord(s[3]) - 48)
 out.append(tbl[x])
 accepted += 1
 i += b
 return out, i, accepted, rejected
# (i^z tetration)
def generate_rng_values(
 N_total: int = 100_000,
 D_frac: int = 1600, # fractional digits per component
 A: int = 100, # base skip
 slope: float = 0.05, # skip growth
 use_both: bool = True, # use both Re and Im
 per_level_quota: int = 4096, # max outputs to attempt per tetration
 verbose: bool = False, # debug
):
 t0 = time.time()
 mp.mp.dps = max(60, int(D_frac * 1.05) + 40)
 x = mp.mpf("0") # Re
 y = mp.mpf("1") # Im
 values = bytearray()
 total_levels = 0
 total_consumed = 0
 total_accepted = 0
 total_rejected = 0
 exp = mp.exp
 cospi = mp.cospi
 sinpi = mp.sinpi
 pi = mp.pi
 # Check milestones (Had to do this for setting growth>12.66 since hitting digit limit and program stalls)
 milestone = 10_000
 next_print = milestone
 while len(values) < N_total:
 # closed-form tetration faster than exponentiation
 r = exp(-(pi * y) / 2)
 half_x = x / 2
 x = r * cospi(half_x)
 y = r * sinpi(half_x)
 total_levels += 1
 s = A + math.ceil(slope * total_levels)
 level_total_digits = (2 * D_frac) if use_both else (2 * D_frac)
 if s >= level_total_digits:
 continue
 take = min(per_level_quota, N_total - len(values))
 max_digits = level_total_digits - s
 need_digits = min(4 * take, max_digits)
 if need_digits <= 0:
 continue
 # Only the needed slice without full strings
 stream = stream_after_skip(x, y, D_frac, skip=s, need_digits=need_digits, use_both=use_both)
 vals, consumed, acc, rej = extract_mod101(stream, need=take, block=4)
 values.extend(vals)
 total_consumed += consumed
 total_accepted += acc
 total_rejected += rej
 if verbose and len(values) >= next_print:
 now = len(values)
 print(f"Progress: {now:,} / {N_total:,} ({100*now/N_total:.1f}%) in {time.time() - t0:.2f}s", flush=True)
 next_print += milestone
 t1 = time.time()
 stats = {
 "levels_used": total_levels,
 "digits_per_component": D_frac,
 "stream_chars_consumed": total_consumed,
 "blocks_accepted": total_accepted,
 "blocks_rejected": total_rejected,
 "accept_rate": total_accepted / max(1, (total_accepted + total_rejected)),
 "elapsed_sec_generation": t1 - t0,
 }
 return values, stats
# Eval
def shannon_entropy_bits(values, alphabet_size=101) -> float:
 N = len(values)
 if N == 0:
 return 0.0
 counts = Counter(values)
 H = 0.0
 for i in range(alphabet_size):
 cnt = counts.get(i, 0)
 if cnt:
 p = cnt / N
 H -= p * math.log2(p)
 return H
# Deflate
def compressibility_ratio(values_bytes: bytes, level=9) -> float:
 uncompressed = len(values_bytes)
 if uncompressed == 0:
 return 0.0
 compressed = len(zlib.compress(values_bytes, level))
 return compressed / uncompressed
def chi_square_uniform(values, alphabet_size=101):
 N = len(values)
 expected = N / alphabet_size
 counts = Counter(values)
 chi2 = 0.0
 for i in range(alphabet_size):
 obs = counts.get(i, 0)
 chi2 += (obs - expected) ** 2 / expected if expected > 0 else 0.0
 return chi2, expected
def pack_base101_6to5(vals):
 out = bytearray((len(vals) + 5) // 6 * 5)
 i = j = 0
 n = len(vals)
 while i < n:
 # first triplet
 a = vals[i]; b = vals[i+1] if i+1 < n else 0; c = vals[i+2] if i+2 < n else 0
 num1 = a*10201 + b*101 + c
 i += 3
 # second triplet
 d = vals[i] if i < n else 0; e = vals[i+1] if i+1 < n else 0; f = vals[i+2] if i+2 < n else 0
 num2 = d*10201 + e*101 + f
 i += 3
 v = (num1 << 20) | num2
 out[j] = (v >> 32) & 0xFF
 out[j+1] = (v >> 24) & 0xFF
 out[j+2] = (v >> 16) & 0xFF
 out[j+3] = (v >> 8) & 0xFF
 out[j+4] = v & 0xFF
 j += 5
 return bytes(out)
# Run entrypoint
if __name__ == "__main__":
 N_TARGET = 100_000
 D_FRAC = 1600
 A_SKIP = 100
 SLOPE = 0.2
 USE_BOTH = True
 PER_LEVEL = 4096
 VERBOSE = False
 vals_ba, gen_stats = generate_rng_values(
 N_total=N_TARGET,
 D_frac=D_FRAC,
 A=A_SKIP,
 slope=SLOPE,
 use_both=USE_BOTH,
 per_level_quota=PER_LEVEL,
 verbose=VERBOSE,
 )
 H_bits = shannon_entropy_bits(vals_ba, alphabet_size=101)
 chi2, expected = chi_square_uniform(vals_ba, alphabet_size=101)
 ratio = compressibility_ratio(bytes(vals_ba), level=9)
 packed_ratio = compressibility_ratio(pack_base101_6to5(vals_ba), level=9)
 print(f"Sample size: {N_TARGET}")
 print(f"Entropy: {H_bits:.6f} (ideal entropy = {math.log2(101):.6f})")
 print(f"DEFLATE: {ratio:.6f} (closer to 1 == better)")
 print(f"DEFLATE (Byte-packed) {packed_ratio:.6f} (Header even increases size!)")
 print(f"Chi-square: {chi2:.2f} (df=100, expected per bin ≈ {expected:.2f})")
 print("Generator stats")
 for k, v in gen_stats.items():
 if isinstance(v, float):
 print(f"{k:>24}: {v:.6f}")
 else:
 print(f"{k:>24}: {v}")
 vals_list = list(vals_ba)
 print("First 25 values:", vals_list[:25])
 print("Last 25 values:", vals_list[-25:])
 counts = Counter(vals_ba)
 lo = min(counts.values()); hi = max(counts.values())
 print(f"Bin count range: min={lo}, max={hi}")
79781319
1

I used a Permuted Congruential Generator which is, according to D. Johnston - Random Number Generators - Principles and Practices § 6.4, "the state of the art in uniform noncryptographic random number generators".

Permuted Congruential Generators use an internal LCG followed by a permutation function and a truncation as explained in the paper https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf

To generate the seed, in order to keep it simple and portable, I merely used an hash of the process id, the system time in nanoseconds, and the number of nanoseconds elapsed to update the hash with the two previous values.

One of the main challenges was to translate uniformly a distribution between 0 and 2**32-1 into a distribution between 0 and 100 knowing that the length of the former is not a multiple of the length of the later. I used an unbiased algorithm described in https://www.pcg-random.org/posts/bounded-rands.html

The more important challenge was to compute the compression ratio on a byte sequence without introducing extra pattern due to the storage itself which leads to a lower measurement than on raw consecutive data, see below.

I obtained the following results:

Shannon entropy: 6.657494
Compression ratio: 1.000180

The ideal entropy of a uniform distribution of the 101 values between 0 and 100 inclusive is:

math.log2(101) = 6.658211482751795

NB: In the first iteration I obtained a compression ratio of 0.843 which was not as high as expected, but this was because the values are generated in the inclusive range 0-100, so the high order bit of each byte of the generated sequence is always 0. To check this not so high compression ratio was due to the storage/encoding of the sequence, I generated the values in the inclusive range 0-255 instead for which I obtained a compression ratio slightly higher than 1.0. Then to compute the compression ratio on the generated sequence itself without adding extra bits for storage I stored each sequence of 6 consecutive values between 0 and 100 inclusive into 5 consecutive bytes. Because the equation 101**n = 256**(n-1) solves to n = ceil(1 / (1 - log2(101) / 8)) = 6 so that the number sum_{0<=k<=5}(v_k * 101**k) accumulating 6 consecutive values < 101 can be stored into 5 bytes.

Below is the code in Rust.

Random number generator implemented inpcg32.rs:

// Permuted Congruential Generator
// Ref: <https://www.pcg-random.org>
// 32-bit output with 64-bit state.
pub struct Pcg32 {
 state: u64,
}
impl Pcg32 {
 // The multiplier and the increment values of the internal LCG
 // are the default ones of the reference implementation.
 // Ref: <https://github.com/imneme/pcg-cpp/blob/428802d1a5634f96bcd0705fab379ff0113bcf13/include/pcg_random.hpp#L158>
 const MULTIPLIER: u64 = 6364136223846793005;
 const INCREMENT: u64 = 1442695040888963407;
 pub fn new(seed: u64) -> Self {
 Self { state: seed }
 }
 pub fn sample(&mut self) -> u32 {
 // Internal LCG
 self.state = self
 .state
 .wrapping_mul(Self::MULTIPLIER)
 .wrapping_add(Self::INCREMENT);
 // XorSHift high bits, Random Rotation (XSH-RR)
 // Ref: <https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf>
 // 6.3.1 32-bit Output, 64-bit State: PCG-XSH-RR
 (((self.state ^ (self.state >> 18)) >> 27) as u32)
 .rotate_right((self.state >> 59) as u32)
 }
}

Seed generator implemented in: seed.rs:

use std::hash::{DefaultHasher, Hasher};
use std::process;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
pub fn generate_seed() -> u64 {
 let instant_0 = Instant::now();
 let mut hasher = DefaultHasher::new();
 hasher.write_u32(process::id());
 hasher.write_u128(match SystemTime::now().duration_since(UNIX_EPOCH) {
 Ok(d) => d.as_nanos(),
 _ => 0_u128,
 });
 hasher.write_u128(instant_0.elapsed().as_nanos());
 hasher.finish()
}

Shannon entropy computation implemented in shannon_entropy.rs:

pub fn get_shannon_entropy_from_distribution(dist: &[usize]) -> f64 {
 let total = dist.iter().sum::<usize>() as f64;
 assert_ne!(total, 0.0);
 let mut res = 0.0;
 for n in dist {
 if *n == 0 {
 continue;
 }
 let p = *n as f64 / total;
 res -= p * p.log2();
 }
 res
}
#[cfg(test)]
mod tests {
 use super::*;
 const EPS: f64 = 1e-6;
 #[test]
 fn get_shannon_entropy_from_distribution_unique() {
 let mut dist = [0; 100];
 dist[42] = 100000;
 assert!(get_shannon_entropy_from_distribution(&dist).abs() < EPS);
 }
 #[test]
 fn get_shannon_entropy_from_distribution_uniform_2() {
 let dist = [1000, 1000];
 assert!(
 (get_shannon_entropy_from_distribution(&dist) - 1.0).abs() < EPS
 );
 }
 #[test]
 fn get_shannon_entropy_from_distribution_uniform_128() {
 let dist = [10000; 128];
 assert!(
 (get_shannon_entropy_from_distribution(&dist) - 7.0).abs() < EPS
 );
 }
 #[test]
 fn get_shannon_entropy_from_distribution_uniform_101() {
 let dist = [123456; 101];
 assert!(
 (get_shannon_entropy_from_distribution(&dist) - 6.6).abs() < 0.1
 );
 }
}

main.rs:

use deflate::deflate_bytes;
mod pcg32;
use pcg32::Pcg32;
mod seed;
use seed::generate_seed;
mod shannon_entropy;
use shannon_entropy::get_shannon_entropy_from_distribution;
// Use a Pcg32 to generate a random value between 0 and `max_value` inclusive.
// Multiple calls generate a uniform distribution in this range while avoiding
// modulo bias.
fn sample_uniform(rng: &mut Pcg32, max_value: u32) -> u32 {
 if max_value == u32::MAX {
 // Full range, no need to debias.
 return rng.sample();
 }
 // Use Debiased Modulo (Twice), OpenBSD’s method
 // as described in <https://www.pcg-random.org/posts/bounded-rands.html>
 let upper_bound = max_value + 1;
 let min_raw_sample = 0_u32.wrapping_sub(upper_bound) % upper_bound;
 loop {
 let raw_sample = rng.sample();
 if raw_sample >= min_raw_sample {
 return raw_sample % upper_bound;
 }
 }
}
#[derive(Copy, Clone, Debug)]
struct RngEvaluationResult {
 shannon_entropy: f64,
 compression_ratio: f64,
}
fn evaluate_rng(rng: &mut Pcg32, sequence_len: usize) -> RngEvaluationResult {
 // We store the generated values in a packed sequence
 // in order to measure the compression ratio of the generated sequence
 // itself without introducing extra bits at 0 due to the storage.
 // The equation 101**n = 256**(n-1)
 // solves to n = ceil(1 / (1 - log2(101) / 8)) = 6
 // which means that we can store 6 values each between 0-100 inclusive
 // inside 5 bytes.
 let mut packed_sequence =
 Vec::<u8>::with_capacity(5 * sequence_len.div_ceil(6));
 // We use a 64 bits accumulator
 // to compute sum_{0<=k<=5}(v_k * 101**k) < 101**6 which fit inside 64 bits,
 // before storing its lower 40 bits into 5 bytes of the packed sequence.
 let mut accumulator: u64 = 0;
 // We do not use Horner method in order to keep the order of the sequence.
 let muls = [
 1,
 101,
 101 * 101,
 101 * 101 * 101,
 101 * 101 * 101 * 101,
 101 * 101 * 101 * 101 * 101,
 ];
 let mut k: usize = 0;
 // store the accumulated 5 bytes into dst vector
 fn store_acc(acc: u64, dst: &mut Vec<u8>) {
 for i in 0..5 {
 dst.push((acc >> (8 * i)) as u8);
 }
 }
 let mut distribution = vec![0_usize; 101];
 for _ in 0..sequence_len {
 let val = sample_uniform(rng, 100);
 distribution[val as usize] += 1;
 accumulator += val as u64 * muls[k];
 k += 1;
 if k == 6 {
 store_acc(accumulator, &mut packed_sequence);
 accumulator = 0;
 k = 0;
 }
 }
 if k > 0 {
 store_acc(accumulator, &mut packed_sequence);
 }
 RngEvaluationResult {
 shannon_entropy: get_shannon_entropy_from_distribution(&distribution),
 compression_ratio: deflate_bytes(&packed_sequence).len() as f64
 / packed_sequence.len() as f64,
 }
}
fn main() {
 let seed = generate_seed();
 let mut rng = Pcg32::new(seed);
 let ret = evaluate_rng(&mut rng, 100_000);
 println!("Shannon entropy: {:.6}", ret.shannon_entropy);
 println!("Compression ratio: {:.6}", ret.compression_ratio);
}

Cargo.toml:

[package]
name = "random_number_generator"
version = "0.1.0"
edition = "2024"
[dependencies]
deflate = "1.0.0"
79781044
1

JavaScript random number generator (xorshift)

In this entry, I coded a 32-bit xorshift pseudorandom number generator (PRNG) in JavaScript, seeded with entropy extracted from measurements of time change in JavaScript Date objects. I used rejection sampling to get integers in the range 0-100 with equal probability.

For 100 000 random numbers, the Shannon entropy was 6.657 and the compression ratio 1.00 (when the randomly generated seed was 0x2a1898ff).

An interesting part of the challenge was to come up with an entropy extractor for the seed. It turns out that running a for loop while counting the number of iterations until the date value changes is quite unpredictable in JavaScript. I extracted random bits from pairs of these iteration counts, using von Neumann correction. I figured that there would be more unpredictability in the least significant bits, so I only extracted the lower bits until encountering the first pair of bits that did not differ.

This way to extract random bits is quite slow and expensive, my laptop needs about 50 milliseconds to extract 32 bits of entropy to seed the PRNG.

Out of curiosity, I took the time to generate another 100 000 random numbers using the entropy extractor directly as an RNG, also resulting in a Shannon entropy of 6.657 and a compression ratio of 1.00.

Code

const seed = extractEntropy(32);
const rng = xorshift32(seed);
const random1e5Numbers = randomBetween0and100(1e5, rng);
function xorshift32(seed) {
 if (seed !== 0) {
 let state = seed;
 return function () {
 state ^= state << 13;
 state ^= state >> 17;
 state ^= state << 5;
 return state;
 };
 }
}
function extractEntropy(bits) {
 let output = 0;
 bits = Math.min(bits, 32);
 while (bits > 0) {
 // count loop iterations until date value changes, twice:
 let count1 = 0,
 count2 = 0;
 for (let time = +new Date(); time === +new Date(); count1++);
 for (let time = +new Date(); time === +new Date(); count2++);
 // output the lower bits that differ, using von Neumann correction:
 for(
 let bitPosition = 0; 
 (count1 & (1 << bitPosition)) ^ (count2 & (1 << bitPosition));
 bitPosition++
 ){
 output = (output << 1) | ((count2 >> bitPosition) & 1);
 bits--;
 }
 }
 return output;
}
function randomBetween0and100(amount, rng){
 let output = [],
 generated = 0;
 while(generated < amount){
 // rejection sampling, discarding 20-bit integers larger 
 // than any 3-digit number in base 101:
 let random20Bits = 0xFFFFF;
 while(random20Bits >= 101**3){
 random20Bits = rng() & 0xFFFFF;
 }
 // convert bits to digits in base 101:
 output[generated++] = Math.floor(random20Bits / (101**2)) % 101;
 output[generated++] = Math.floor(random20Bits / 101) % 101;
 output[generated++] = random20Bits % 101;
 }
 output.length = amount;
 return output;
}

Code used to test the distribution of randomness

function testRandomness(data){
 let serializedData = randomNumbers2Bytes(data);
 compress(serializedData).then(function(compressedData){
 console.log(
`seed: 0x${(seed >>> 0).toString(16)}
compressed data length: ${compressedData.length}
serialized data length: ${serializedData.length}
compression ratio: ${compressedData.length/serializedData.length}
entropy: ${ShannonEntropy(data)}`
 );
 })
}
function ShannonEntropy(data){
 let frequencies = {},
 index,
 se = 0;
 for(index = 0; index < data.length; index++){
 frequencies[data[index]] = (frequencies[data[index]] || 0) + 1;
 }
 for(index in frequencies){
 se += (frequencies[index]/data.length) * 
 Math.log2(frequencies[index]/data.length);
 }
 return -se;
}
async function compress(data) {
 const compressedStream = new Response(data).body.pipeThrough(
 new CompressionStream("deflate-raw")
 );
 return await new Response(compressedStream).bytes();
}
function randomNumbers2Bytes(rn){
 /*
 For binary serialization, the 100 000 random numbers 
 between 0 and 100 can be converted back to 20-bit integers.
 Pairs of these integerss (40 bits) can then be written as 5 bytes.
 */
 let len = Math.ceil(rn.length/6)*5;
 let bytes = new Uint8Array(len);
 for(let index = 0, position = 0; index < rn.length; index += 6){
 let num1 = (rn[index] | 0) * 101**2 + 
 (rn[index + 1] | 0) * 101 +
 (rn[index + 2] | 0);
 let num2 = (rn[index + 3] | 0) * 101**2 + 
 (rn[index + 4] | 0) * 101 +
 (rn[index + 5] | 0);
 let num3 = num1 * 2**20 + num2;
 bytes[position++] = Math.floor(num3/256**4) % 256;
 bytes[position++] = Math.floor(num3/256**3) % 256;
 bytes[position++] = Math.floor(num3/256**2) % 256;
 bytes[position++] = Math.floor(num3/256) % 256;
 bytes[position++] = num3 % 256;
 }
 return bytes;
}
79775955
1
  • 4.3k
  • 1
  • 25
  • 30

Random number generator based on Xorshift.

#!/usr/bin/env python3
from random import getrandbits
from zlib import compress
import numpy as np
# Number of random numbers to generate for testing
COUNT = 100000
class XORSHIFT:
 """Xorshift random number generator."""
 bits = 64
 def __init__(self, seed=271828182):
 """Initialize with a seed and set the bitmask."""
 self.bitmask = 2**self.bits - 1
 self.rand = seed & self.bitmask # Ensure max bits integer
 self.m = 101 # modulus
 def get_rand(self):
 """Generate the next random number.
 Based on https://en.wikipedia.org/wiki/Xorshift"""
 self.rand = self.rand ^ (self.rand << 13)
 self.rand = self.rand ^ (self.rand >> 7)
 self.rand = self.rand ^ (self.rand << 17)
 self.rand = self.rand & self.bitmask # Ensure max bits integer
 return self.rand % self.m
 @staticmethod
 def generate_seed():
 """Generate a random seed with the specified number of bits."""
 return getrandbits(XORSHIFT.bits)
 @staticmethod
 def calculate_entropy(rands=None, base=None):
 """Calculate the entropy of the random numbers."""
 _, rand_counts = np.unique(rands, return_counts=True)
 normalized_counts = rand_counts / rand_counts.sum()
 base = np.e if base is None else base
 return -(normalized_counts * np.log(normalized_counts) / np.log(base)).sum()
 @staticmethod
 def calculate_compressionratio(rands=None):
 """Calculate the compression ratio of the random numbers."""
 bytes_rands = bytes(rands)
 bytes_compressed = compress(bytes_rands)
 count_bytes = len(bytes_rands)
 count_compressed = len(bytes_compressed)
 return count_compressed / count_bytes
def main():
 """Test the XORSHIFT random number generator."""
 xorshift = XORSHIFT(XORSHIFT.generate_seed())
 rands = [xorshift.get_rand() for _ in range(COUNT)]
 print(f"Entropy: {XORSHIFT.calculate_entropy(rands, 2) :.3f}")
 print(f"Compressed/Uncompressed: {XORSHIFT.calculate_compressionratio(rands) :.3f}")
if __name__ == "__main__":
 main()

Results are

Entropy: 6.658
Compressed/Uncompressed: 0.843
79774430
2

I built a simple random number generator using the Linear Congruential Generator (LCG) method. To make the output less predictable, I used the current system time as the seed. It generated 100,000 numbers between 0 and 100. I then checked how random they were using entropy and compression tests.

Entropy & Compression Results: The entropy came out close to 6.6 bits, which means the numbers were pretty evenly spread. When I compressed the data using Java’s Deflater, the ratio was around 1.01—barely compressible, which is a good sign of randomness.

Code :

import java.util.*;
import java.util.zip.Deflater;
public class RandomLCG {
 public static void main(String[] args) {
 int count = 100000;
 int[] numbers = new int[count];
 
 long seed = System.currentTimeMillis();
 long a = 1103515245;
 long c = 12345;
 long m = (long)Math.pow(2, 31);
 // Generating the numbers
 for (int i = 0; i < count; i++) {
 seed = (a * seed + c) % m;
 numbers[i] = (int)(seed % 101); // 0–100
 }
 // Entropy-calc
 Map<Integer, Integer> freq = new HashMap<>();
 for (int num : numbers) {
 freq.put(num, freq.getOrDefault(num, 0) + 1);
 }
 double entropy = 0.0;
 for (int f : freq.values()) {
 double p = (double) f / count;
 entropy -= p * (Math.log(p) / Math.log(2));
 }
 System.out.printf("Entropy: %.4f bits%n", entropy);
 // Compression-test
 byte[] byteData = new byte[count];
 for (int i = 0; i < count; i++) {
 byteData[i] = (byte) numbers[i];
 }
 Deflater deflater = new Deflater();
 deflater.setInput(byteData);
 deflater.finish();
 byte[] compressed = new byte[count];
 int compressedSize = deflater.deflate(compressed);
 double ratio = (double) compressedSize / byteData.length;
 System.out.printf("Compression Ratio: %.4f%n", ratio);
 }
}
79774011
0
import time
import zlib
import math
from collections import Counter
# Constants for the LCG
A = 1103515245
C = 12345
M = 2**31
# Get entropy from system time and user typing delay
def get_seed():
 print("Press ENTER twice to generate timing-based seed.")
 input("First ENTER: ")
 t1 = time.time_ns()
 input("Second ENTER: ")
 t2 = time.time_ns()
 seed = t2 - t1 + t1
 return seed % M
# LCG implementation
def lcg(seed, count=100_000):
 nums = []
 x = seed
 for _ in range(count):
 x = (A * x + C) % M
 nums.append(x % 101) # Scale to [0, 100]
 return nums
# Shannon entropy calculation
def shannon_entropy(data):
 freq = Counter(data)
 total = len(data)
 entropy = -sum((count / total) * math.log2(count / total) for count in freq.values())
 return entropy
# Compressibility test
def compressibility_test(data):
 byte_data = bytes(data) # Assumes values fit in a byte (0-100)
 compressed = zlib.compress(byte_data)
 return len(compressed) / len(byte_data)
def main():
 seed = get_seed()
 print(f"Seed: {seed}")
 data = lcg(seed)
 
 # Entropy
 entropy = shannon_entropy(data)
 print(f"Shannon Entropy: {entropy:.4f} bits (Max: {math.log2(101):.4f})")
 # Compressibility
 compression_ratio = compressibility_test(data)
 print(f"Compression Ratio: {compression_ratio:.4f}")
if __name__ == "__main__":
 main()
79783994
0

Can you share your the outputs from your code?

79772803
1

This PHP script generates pseudo-random numbers using a custom seeding function based on high-resolution timestamps. It combines microtime() and hrtime() to produce a floating-point seed, hashes it with sha256, and converts part of the hash into a numeric value. The randomNumber() function then maps these seeds into integers within a specified range.

Additionally, the script includes basic randomness evaluation:

dataCompressionTest() checks compressibility to infer entropy.

enthropyTest() counts occurrences of each number to show distribution.

This approach attempts to improve uniqueness and unpredictability compared to simpler PRNGs by leveraging precise timing and hashing.

<?php 
function randomNumber( float $start=0, float $end=100): array
{
 $randomNumbers = [];
 $i = 0;
 while($i< 100){
 $seed = seed();
 $randomNumbers[] = ($start + intval(fmod($seed, ($end - $start + 1))));
 $i++;
 }
 return $randomNumbers;
}
function seed(): float
{
 $seed = microtime(true);
 $ns = hrtime(true);
 $seedString = hash('sha256', $seed . $ns);
 return hexdec(substr($seedString, 0, 15)) / 1e15;
}
function index(){
 $ans = randomNumber(0,100);
 print_r($ans) . ", ";
 echo "\n ===================DATA COMPRESSION TEST=================== \n";
 dataCompressionTest($ans);
 echo "\n ===================ENTHROPY TEST=================== \n";
 enthropyTest($ans);
 
}
function enthropyTest($results): void
{
 $results = array_map('intval', $results);
 $counts = array_count_values($results);
 ksort($counts);
 print_r($counts);
}
function dataCompressionTest($data): void
{
 $originalSize = strlen(serialize($data));
 $gzData = gzcompress(serialize($data));
 $compressedSize = strlen($gzData);
 echo "Original Size: $originalSize bytes\n";
 echo "Compressed Size: $compressedSize bytes\n";
 echo "Compression Ratio: " . ($compressedSize / $originalSize) . "\n";
}
index();
PS C:\Users\USER 1\Desktop\MUGWANEZA MANZI Audace\stack-overflo> php .\index.php
Array
(
 [0] => 88
 [1] => 60
 [2] => 84
 [3] => 41
 [4] => 66
 [5] => 93
 [6] => 25
 [7] => 12
 [8] => 99
 [9] => 79
 [10] => 53
 [11] => 8
 [12] => 71
 [13] => 46
 [14] => 15
 [15] => 6
 [16] => 53
 [17] => 5
 [18] => 2
 [19] => 97
 [20] => 9
 [21] => 60
 [22] => 43
 [23] => 51
 [24] => 9
 [25] => 11
 [26] => 50
 [27] => 82
 [28] => 94
 [29] => 31
 [30] => 48
 [31] => 39
 [32] => 6
 [33] => 54
 [34] => 88
 [35] => 10
 [36] => 34
 [37] => 34
 [38] => 3
 [39] => 77
 [40] => 96
 [41] => 5
 [42] => 72
 [43] => 41
 [44] => 81
 [45] => 77
 [46] => 98
 [47] => 100
 [48] => 32
 [49] => 30
 [50] => 48
 [51] => 35
 [52] => 79
 [53] => 8
 [54] => 57
 [55] => 17
 [56] => 17
 [57] => 21
 [58] => 72
 [59] => 11
 [60] => 62
 [61] => 72
 [62] => 74
 [63] => 19
 [64] => 55
 [65] => 92
 [66] => 52
 [67] => 36
 [68] => 42
 [69] => 86
 [70] => 33
 [71] => 22
 [72] => 43
 [73] => 17
 [74] => 43
 [75] => 16
 [76] => 26
 [77] => 18
 [78] => 73
 [79] => 19
 [80] => 31
 [81] => 88
 [82] => 97
 [83] => 9
 [84] => 31
 [85] => 10
 [86] => 28
 [87] => 59
 [88] => 63
 [89] => 68
 [90] => 38
 [91] => 64
 [92] => 2
 [93] => 41
 [94] => 32
 [95] => 69
 [96] => 10
 [97] => 88
 [98] => 35
 [99] => 50
)
 ===================DATA COMPRESSION TEST===================
Original Size: 987 bytes
Compressed Size: 368 bytes
Compression Ratio: 0.37284701114488
 ===================ENTHROPY TEST===================
Array
(
 [2] => 2
 [3] => 1
 [5] => 2
 [6] => 2
 [8] => 2
 [9] => 3
 [10] => 3
 [11] => 2
 [12] => 1
 [15] => 1
 [16] => 1
 [17] => 3
 [18] => 1
 [19] => 2
 [21] => 1
 [22] => 1
 [25] => 1
 [26] => 1
 [28] => 1
 [30] => 1
 [31] => 3
 [32] => 2
 [33] => 1
 [34] => 2
 [35] => 2
 [36] => 1
 [38] => 1
 [39] => 1
 [41] => 3
 [42] => 1
 [43] => 3
 [46] => 1
 [48] => 2
 [50] => 2
 [51] => 1
 [52] => 1
 [53] => 2
 [54] => 1
 [55] => 1
 [57] => 1
 [59] => 1
 [60] => 2
 [62] => 1
 [63] => 1
 [64] => 1
 [66] => 1
 [68] => 1
 [69] => 1
 [71] => 1
 [72] => 3
 [73] => 1
 [74] => 1
 [77] => 2
 [79] => 2
 [81] => 1
 [82] => 1
 [84] => 1
 [86] => 1
 [88] => 4
 [92] => 1
 [93] => 1
 [94] => 1
 [96] => 1
 [97] => 2
 [98] => 1
 [99] => 1
 [100] => 1
)
79772752
1

For this challenge I implemented LCG pseudo-random numbers.

Xn+1 = (aXn + c ) mod m

where,

m = modulus

a = multiplier

c = increment

Js Code:

const zlib = require("zlib");
// LCG setup
let seed = Date.now();
const m = 2**31 - 1, a = 1103515245, c = 12345;
const lcg = n => (seed = (a * seed + c) % m) % n;
// Generate numbers
const nums = Array.from({length: 100000}, () => lcg(101));
// Shannon Entropy
const entropy = (() => {
 const freq = nums.reduce((f,n)=>(f[n]=(f[n]||0)+1,f),{});
 return Object.values(freq).reduce((e,c)=>e-(c/nums.length)*Math.log2(c/nums.length),0);
})();
// Compression Test
const ratio = zlib.deflateSync(Buffer.from(nums)).length / nums.length;
console.log("Entropy:", entropy.toFixed(4));
console.log("Compression Ratio:", ratio.toFixed(4));
79783995
0

Can you share the outputs from the code?

79772581
1

I made a random number generator using a Linear Congruential Generator. It takes a seed from the system time and process ID so it changes on each run. Then I generated 100,000 numbers between 0–100, checked their entropy to see how evenly spread thay are, and tested compression to see if there are hidden patterns.

Entropy: can take approx6.657 bits (almost the max 6.658 =>numbers are well spread).

Compression ratio: approx.0.84 (not perfectly random, but not easily compressible either).

import time, os, zlib, math, collections
def lcg(seed, a=1664525, c=1013904223, m=2**32):
 while True:
 seed = (a * seed + c) % m
 yield seed
seed = (time.time_ns() ^ os.getpid()) & 0xffffffff
gen = lcg(seed)
nums = [next(gen) % 101 for _ in range(100_000)]
counts = collections.Counter(nums)
probs = [c/len(nums) for c in counts.values()]
entropy = -sum(p * math.log2(p) for p in probs)
data = bytes(nums)
ratio = len(zlib.compress(data)) / len(data)
print(f"Entropy: {entropy:.6f} bits")
print(f"Copression ratio: {ratio:.3f}")
79772396
2

In python, implementing LCG with a, c and m values from LCG Parameters in common use, I get a shannon entropy value of 6.64 and compressibility ratio of 0.82.

import numpy as np
n = 100000
x = 3
A = np.empty(n, dtype=np.uint8)
def LGC(x): # Values from Wikipedia
 a = 16843009 
 c = 3014898611
 m = 2**32
 return (a*x+c) % m
def shannon_entropy(arr, symbols=100):
 counts = np.bincount(arr, minlength=symbols)
 p = counts / counts.sum()
 p_nonzero = p[p > 0]
 return -np.sum(p_nonzero * np.log2(p_nonzero))
for i in range(n):
 x = LGC(x)
 A[i] = x % 100
Shannon = shannon_entropy(A, symbols=101)
data_bytes_A = A.tobytes()
ratioA = len(zlib.compress(data_bytes_A)) / len(data_bytes_A)
print("Shannon A:", Shannon)
print("Ratio A:", ratioA)
79772199
1

Something interesting: storing the these integers in a numpy array uses many more bytes than a python list (8 times more to be exact). Compression ratio when using numpy arrays is around 0.175 but around 0.84 when using python lists

Code output:

Challenge #9: Random Number Generator Shannon index: 6.6426618991771 Compressed size: 140242 Original size: 800000 Compression ratio: 0.17513

Comparison with numpy.random integers Shannon index: 6.642860340229002 Compressed size: 140238 Original size: 800000 Compression ratio: 0.1753025

Code:

from time import time
import numpy as np
from hashlib import sha256
from zlib import compress
def rng(upper=100):
 # not a real seed 
 seed = str(time()).encode()
 # encrypt the seed str with sha256
 hexstr = sha256(seed).hexdigest()
 # sum the ascii epresentation of the encrypted time
 _sum = sum((char.encode('ascii')[0] for char in hexstr))
 # return the modulo to the bound. 
 # Note, this gets less random as 'upper' gets bigger, not a problem until maybe ~1000
 return _sum % upper
def shannon(numbers):
 _, counts = np.unique(numbers, return_counts=True)
 p = counts/len(numbers)
 return -(p * np.log2(p)).sum()
 
def run(numbers):
 print("Shannon index:", shannon(numbers))
 compressed_object = compress(numbers)
 print("Compressed size:", len(compressed_object))
 print("Original size:", len(bytes(numbers)))
 print("Compression ratio:", len(compressed_object)/len(bytes(numbers)))
if __name__=="__main__":
 
 print("Challenge #9: Random Number Generator")
 random_numbers = np.array([rng() for i in range(100_000)])
 run(random_numbers)
 print("\nComparison with numpy.random integers")
 rng = np.random.default_rng()
 random_numbers = rng.integers(0, 100, size=100_000)
 run(random_numbers)
79772017
0
import time
import math
import zlib
# Linear Congruential Generator
class LCG:
 def __init__(self, seed=None):
 if seed is None:
 seed = int(time.time_ns())
 self.state = seed
 self.a = 1664525
 self.c = 1013904223
 self.m = 2**32
 def next(self):
 self.state = (self.a * self.state + self.c) % self.m
 return self.state
 def randint(self, low, high):
 return low + self.next() % (high - low + 1)
# Generate 100,000 random numbers between 0 and 100
rng = LCG()
numbers = [rng.randint(0, 100) for _ in range(100000)]
# Shannon Entropy
def shannon_entropy(data):
 freq = {}
 for n in data:
 freq[n] = freq.get(n, 0) + 1
 entropy = 0
 for count in freq.values():
 p = count / len(data)
 entropy -= p * math.log2(p)
 return entropy
entropy = shannon_entropy(numbers)
# Compression Test
data_bytes = bytes(numbers)
compressed = zlib.compress(data_bytes)
compression_ratio = len(compressed) / len(data_bytes)
print("Shannon Entropy:", entropy)
print("Compression Ratio:", compression_ratio)
Results
 * Shannon Entropy: ~6.63 bits (close to the theoretical maximum of log2(101) ≈ 6.66)
 * Compression Ratio: ~0.99 (very close to 1.0, meaning the data is hard to compress and thus more random-like)
Notes
 * Using system time in nanoseconds as the seed ensures that each run produces a different sequence.
 * The entropy and compression results suggest the RNG produces a reasonably uniform and non-compressible distribution.
79771916
0
import time, os, zlib, math, hashlib
from collections import Counter
def gather_jitter_seed(rounds: int = 256, path="/mnt/data/rng_jitter.tmp"):
 buf = bytearray()
 pid = os.getpid()
 for i in range(rounds):
 t0 = time.perf_counter_ns()
 
 s = 0
 iters = 1000 + (i % 17) * 29
 for k in range(iters):
 s ^= (k * 0x9E3779B97F4A7C15) & 0xFFFFFFFFFFFFFFFF
 t1 = time.perf_counter_ns()
 
 payload = (f"{pid}-{i}-{t0}-{t1}-{s}").encode("utf-8")
 with open(path, "wb") as f:
 f.write(payload)
 t2 = time.perf_counter_ns()
 buf += t0.to_bytes(8, "little", signed=False)
 buf += t1.to_bytes(8, "little", signed=False)
 buf += t2.to_bytes(8, "little", signed=False)
 digest = hashlib.sha256(buf).digest()
 seed = int.from_bytes(digest[:8], "little", signed=False)
 return seed
M = 1 << 64
A = 6364136223846793005
C = 1442695040888963407 
class LCG:
 def __init__(self, state: int):
 self.state = state & (M - 1)
 def next_u64(self) -> int:
 self.state = (A * self.state + C) & (M - 1)
 return self.state
def next_0_100(rng: LCG) -> int:
 bound = 101
 threshold = (M // bound) * bound 
 while True:
 x = rng.next_u64()
 if x < threshold:
 return x % bound
def generate_and_measure(N=100_000):
 seed = gather_jitter_seed()
 rng = LCG(seed)
 vals = [next_0_100(rng) for _ in range(N)]
 with open("/mnt/data/lcg_random_100k_0_100.txt", "w") as f:
 f.write("\n".join(map(str, vals)))
 with open("/mnt/data/lcg_random_100k_0_100.bin", "wb") as f:
 f.write(bytes(vals))
 counts = Counter(vals)
 entropy = 0.0
 for k in range(101):
 p = counts.get(k, 0) / N
 if p > 0:
 entropy -= p * math.log2(p)
 raw = bytes(vals)
 compressed = zlib.compress(raw, level=9)
 ratio = len(compressed) / len(raw)
 return {
 "seed": seed,
 "min": min(vals),
 "max": max(vals),
 "mean": sum(vals) / N,
 "stdev": (sum((x - (sum(vals)/N))**2 for x in vals) / N) ** 0.5,
 "entropy_bits": entropy,
 "entropy_max_bits": math.log2(101),
 "compress_ratio": ratio,
 "uncompressed_size_bytes": len(raw),
 "compressed_size_bytes": len(compressed),
 }
if __name__ == "__main__":
 print(generate_and_measure())
79784001
0

Could you share your output on the randomness tests?

79770765
0
const zlib = require('zlib');
function createLCG(seed) {
 const m = 2 ** 31;
 const a = 1664525;
 const c = 1013904223;
 let state = seed;
 return function next() {
 state = (a * state + c) % m;
 return state;
 };
}
function generateRandomNumbers(count, rng) {
 const numbers = [];
 for (let i = 0; i < count; i++) {
 numbers.push(rng() % 101);
 }
 return numbers;
}
function calculateShannonEntropy(data) {
 const freqMap = {};
 const total = data.length;
 data.forEach(num => {
 freqMap[num] = (freqMap[num] || 0) + 1;
 });
 let entropy = 0;
 for (let key in freqMap) {
 const p = freqMap[key] / total;
 entropy -= p * Math.log2(p);
 }
 return entropy;
}
function testCompressibility(data) {
 const buffer = Buffer.from(Uint8Array.from(data));
 const compressed = zlib.deflateSync(buffer);
 const ratio = compressed.length / buffer.length;
 return {
 originalSize: buffer.length,
 compressedSize: compressed.length,
 compressionRatio: ratio
 };
}
function main() {
 const seed = Date.now();
 const rng = createLCG(seed);
 console.log("Generating 100,000 random integers (0–100)...");
 const randomNumbers = generateRandomNumbers(100000, rng);
 console.log("Calculating Shannon entropy...");
 const entropy = calculateShannonEntropy(randomNumbers);
 console.log(`Shannon Entropy: ${entropy.toFixed(4)} bits`);
 console.log("Running DEFLATE compressibility test...");
 const compressibility = testCompressibility(randomNumbers);
 console.log(`Original Size: ${compressibility.originalSize} bytes`);
 console.log(`Compressed Size: ${compressibility.compressedSize} bytes`);
 console.log(`Compression Ratio: ${compressibility.compressionRatio.toFixed(4)}`);
}
main();
79784002
0

Could you share your output on the randomness tests?

79770396
1
import time
import zlib
import math
from collections import Counter
a = 1664525
c = 1013904223
m = 2**32
seed = int(time.time_ns() % m)
X = seed
random_numbers = []
for _ in range(100_000):
 X = (a * X + c) % m
 random_numbers.append(X % 101)
counts = Counter(random_numbers)
entropy = -sum((count/100000) * math.log2(count/100000) for count in counts.values())
print("Shannon Entropy:", entropy)
data_bytes = bytes(random_numbers)
compressed = zlib.compress(data_bytes)
ratio = len(compressed)/len(data_bytes)
print("Compression Ratio:", ratio)
79784025
0

Could you share your output on the randomness tests?

79770369
2

I chose to create a random, non-repeating (at least over 100k or so elements) sequence on an interval from 0 to some prime number (p) using modular exponentiation in JavaScript. (The upper limit is actually p - 2, which is a negligible difference for large enough p.) The integers 0 through 100 are generated by taking the remainder after division by 101 (modulus).

The largest prime less than the square root of JavaScript's MAX_SAFE_INTEGER guarantees that

  • every number in the domain occurs exactly once per period (with a judicious selection of the base),
  • the period is very long (94,906,248 elements, 0 through 94,906,247, in this case), and
  • multiplication before the modulus operation does not overflow the ability to represent integers exactly.

The initial random "seed" value is time-based, with the numerical representation reversed, so that milliseconds are the most significant digits.

output (3 runs)

Entropy: 6.657375515824879
Deflate length: 84235
Entropy: 6.657579325813162
Deflate length: 84263
Entropy: 6.657689959313094
Deflate length: 84243

101 values (0 through 100) is about 6.6582 bits of information. Simply discarding the unused range should yield a compression ratio of about 6.6582/8 = 0.83228, or 83,228 bytes from 100,000. The "deflated" size is actually larger for trials with real data, suggesting "deflate" could do no real compression.

rng.js

( function () {
 'use strict';
 var
 rng, // for the generator object
 N = 100000, // # of numbers to generate
 c = new Array( 101 ).fill( 0 ), // to count the distribution
 s = new Uint8Array( N ), // generated sequence
 S = 0, // entropy
 i;
 // generator of a "random" sequence of integers
 function RNG() {
 var
 prime = 94906249, // modulus = largest prime < sqrt( MAX_SAFE_INTEGER )
 base = 9738, // largest primitive of [ 0, p - 1 ] < sqrt( p )
 seed = parseInt(
 Date.now().toString().split( '' ).reverse().join( '' )
 ) % ( prime - 1 );
 // modular exponentiation
 // return b power n mod m
 function exp( b, n, m ) {
 var
 result = 1;
 while ( n > 0 ) {
 if ( n % 2 ) {
 result = ( result * b ) % m;
 }
 n >>>= 1;
 b = ( b * b ) % m;
 }
 return result;
 }
 // get the next sequence element
 this.get = function get() {
 seed = ( seed + 1 ) % ( prime - 1 );
 return exp( base, seed, prime ) % ( prime - 1 );
 };
 }
 rng = new RNG();
 // generate the sequence & count the distribution
 for ( i = 0; i < N; ++i ) {
 s[ i ] = rng.get() % 101; // map to [ 0, 100 ]
 ++c[ s[ i ] ];
 }
 // calculate the entropy = -Sum( p log p )
 for ( i = 0; i < 101; ++i ) {
 if ( c[ i ] ) {
 S -= c[ i ] / N * Math.log2( c[ i ] / N );
 }
 }
 console.log( 'Entropy:', S );
 // deflate the sequence
 // keep only the length info of the result
 new Response( new Blob( [ s.buffer ], { type: 'application/octet-stream' } )
 .stream()
 .pipeThrough( new CompressionStream( 'deflate' ) )
 ).arrayBuffer().then( b => console.log( 'Deflate length:', b.byteLength ) );
} () );

Aside 1

For fun, I tried a smaller prime for sequence generation, specifically one that would yield a period that is some multiple of 101 (pretty much the opposite of desirable).

prime = 607 // yields a period of p - 1 = 606 = 6 x 101
base = 24 // largest primitive of [ 0, 606 ] < sqrt( p )

output (3 runs)

Entropy: 6.658211417126877
Deflate length: 1114
Entropy: 6.658211417126877
Deflate length: 1114
Entropy: 6.658211417126878
Deflate length: 1114

Since the period of the generator is a small multiple of the range, the samples are extremely uniformly distributed. The only non-uniformity comes from 606 not dividing evenly into 100,000. So the Shannon entropy is maximized. However, as expected, the real compression is

1114/83228 = 1.34%

Aside 2

Implementing the generator to use BigInts, loss of precision due to multiplication would be eliminated. Then one could use:

prime = 0x280000000000001 // 5 ×ばつ 2^55 + 1, so ( p - 1 ) divides evenly by 2^53
base = 0x194c5839 // largest primitive of [ 0, p - 1 ] < sqrt( p )

then return the result mod 253 to get all integers ≤ MAX_SAFE_INTEGER.

79770336
1
import java.util.*;
import java.util.zip.Deflater;
public class LCG_RNG {
 // Step 1: Parameters for LCG
 static final long a = 1664525;
 static final long c = 1013904223;
 static final long m = (1L << 32);
 // Step 2: LCG generator method
 static long seed = System.currentTimeMillis(); // creative: can add other entropy
 static int nextInt() {
 seed = (a * seed + c) % m;
 return (int)(seed % 101); // keep values 0–100
 }
 public static void main(String[] args) {
 // Step 3: Generate 100,000 numbers
 int N = 100000;
 int[] counts = new int[101];
 List<Integer> numbers = new ArrayList<>();
 for (int i = 0; i < N; i++) {
 int num = nextInt();
 numbers.add(num);
 counts[num]++;
 }
 // Step 4: Shannon Entropy
 double entropy = 0.0;
 for (int i = 0; i < 101; i++) {
 if (counts[i] > 0) {
 double p = (double) counts[i] / N;
 entropy -= p * (Math.log(p) / Math.log(2));
 }
 }
 System.out.println("Entropy: " + entropy);
 // Step 5: Compression Test
 // Convert numbers to byte array (or string)
 byte[] data = new byte[N];
 for (int i = 0; i < N; i++) {
 data[i] = (byte)(numbers.get(i).intValue());
 }
 // Compress using DEFLATE
 Deflater deflater = new Deflater();
 deflater.setInput(data);
 deflater.finish();
 byte[] buffer = new byte[200000]; // large enough buffer
 int compressedSize = deflater.deflate(buffer);
 double ratio = (double) compressedSize / data.length;
 System.out.println("Compression ratio: " + ratio);
 }
}
79784003
0

Could you share your output on the randomness tests?

AltStyle によって変換されたページ (->オリジナル) /