Skip to main content

Challenge #9: Random Number Generator [COMPLETED]

Created
Active
Viewed 4k times
26 entries
19

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.

26 entries
Sorted by:
79821718
0
import random
global a
a=200,000
while a>100,000:
    random_number=random.randint(0,100)
    print(random_number)
    a=a-1

uses the random module and a while loop to create 100,000 random numbers

79814939
-1
import time, os, zlib, math, collections

# Linear Congruential Generator
def lcg(seed, a=1664525, c=1013904223, m=2**32):
    while True:
        seed = (a * seed + c) % m
        yield seed

# Seed using time and process ID
seed = (time.time_ns() ^ os.getpid()) & 0xffffffff
gen = lcg(seed)

# Generate 100,000 random numbers (0–100)
nums = [next(gen) % 101 for _ in range(100_000)]

# Shannon Entropy
freq = collections.Counter(nums)
entropy = -sum((c/len(nums)) * math.log2(c/len(nums)) for c in freq.values())

# Compression Ratio
data = bytes(nums)
ratio = len(zlib.compress(data)) / len(data)

print(f"Seed: {seed}")
print(f"Entropy: {entropy:.6f} bits (max {math.log2(101):.6f})")
print(f"Compression Ratio: {ratio:.3f}")
79800039
0

ACORN Pseudo-Random Number Generator

ACORN (Additive Congruential Random Number) generator implementation. A k-th order recursive algorithm using modular addition to produce uniform random numbers in [0,1). Simple, minimal state, statistically robust.

Wikipedia

Author's Website

import std/times

type
  AcornRNG* = object
    k: int
    m: int64
    seeds: seq[int64]

proc initAcorn*(k = 14, m = 1 shl 60): AcornRNG =
  result.k = k
  result.m = m
  result.seeds = newSeq[int64](k + 1)

  let time = getTime()
  let s = time.toUnix
  let ns = time.nanosecond.int64
  result.seeds[0] = (s + ns) mod result.m

  # ensure seed is relatively prime (odd when M is power of 2)
  if result.seeds[0] mod 2 == 0: inc result.seeds[0]

  for i in 1..result.seeds.high:
    result.seeds[i] = (abs(result.seeds[i-1] *% 11111) + 8008135) mod m

proc rand*(rng: var AcornRNG): float64 =
  for i in 1 .. rng.k:
    rng.seeds[i] = (rng.seeds[i-1] + rng.seeds[i]) mod rng.m
  result = rng.seeds[rng.k].float64 / rng.m.float64

when isMainModule:
  var rng = initAcorn()
  for i in 1..120:
    echo rng.rand()
79790494
0

for num in numbers

add 1 to number

 for num2 in numbers
    divide num by num2, add 5 * 6**1\2**1\3 * 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

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
-1

@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
1

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
Author

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

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
Author

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
Author

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
Author

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
Author

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
Author

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
Author

Could you share your output on the randomness tests?