mirror of
https://github.com/exaloop/codon.git
synced 2025-06-03 15:03:52 +08:00
169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
# Copyright (C) 2022-2025 Exaloop Inc. <https://exaloop.io>
|
|
|
|
from ..routines import *
|
|
|
|
DEFAULT_POOL_SIZE = 4
|
|
INIT_A = u32(0x43b0d7e5)
|
|
MULT_A = u32(0x931e8875)
|
|
INIT_B = u32(0x8b51f9dd)
|
|
MULT_B = u32(0x58f38ded)
|
|
MIX_MULT_L = u32(0xca01f9dd)
|
|
MIX_MULT_R = u32(0x4973f715)
|
|
XSHIFT = u32(4 * 8 // 2)
|
|
MASK32 = u32(0xFFFFFFFF)
|
|
|
|
def _int_to_uint32_array(n: int):
|
|
if n < 0:
|
|
raise ValueError("expected non-negative integer")
|
|
elif n < int(MASK32):
|
|
return array((u32(n),))
|
|
else:
|
|
return array((u32(n & 0xFFFFFFFF), u32(n >> 32)))
|
|
|
|
def _coerce_to_uint32_array(x):
|
|
if isinstance(x, ndarray):
|
|
if type(x.data[0]) is u32:
|
|
return x.copy()
|
|
|
|
if isinstance(x, str):
|
|
if x.startswith('0x'):
|
|
return _int_to_uint32_array(int(x, base=16))
|
|
elif x.isdigit():
|
|
return _int_to_uint32_array(int(x))
|
|
else:
|
|
raise ValueError("unrecognized seed string")
|
|
|
|
if isinstance(x, int):
|
|
return _int_to_uint32_array(x)
|
|
|
|
if isinstance(x, float):
|
|
compile_error("seed must be integer")
|
|
|
|
if len(x) == 0:
|
|
return empty(0, dtype=u32)
|
|
subseqs = [_coerce_to_uint32_array(v) for v in x]
|
|
return concatenate(subseqs)
|
|
|
|
def hashmix(value: u32, hash_const: u32):
|
|
value ^= hash_const
|
|
hash_const *= MULT_A
|
|
value *= hash_const
|
|
value ^= value >> XSHIFT
|
|
return value, hash_const
|
|
|
|
def mix(x: u32, y: u32):
|
|
result = (MIX_MULT_L * x - MIX_MULT_R * y)
|
|
result ^= result >> XSHIFT
|
|
return result
|
|
|
|
# TODO: use NumPy's default seeding scheme,
|
|
# which falls back to Python's os.urandom()
|
|
def random_seed_time_pid():
|
|
now = _C.seq_time() * 1000
|
|
key = empty((5,), dtype=u32)
|
|
key[0] = u32(now & 0xFFFFFFFF)
|
|
key[1] = u32(now >> 32)
|
|
key[2] = u32(_C.seq_pid())
|
|
now = _C.seq_time_monotonic()
|
|
key[3] = u32(now & 0xFFFFFFFF)
|
|
key[4] = u32(now >> 32)
|
|
return key
|
|
|
|
class SeedSequence:
|
|
entropy: ndarray[u32, 1]
|
|
_spawn_key: Optional[List[int]]
|
|
pool_size: int
|
|
n_children_spawned: int
|
|
pool: ndarray[u32, 1]
|
|
|
|
@property
|
|
def spawn_key(self) -> List[int]:
|
|
if self._spawn_key is not None:
|
|
return self._spawn_key.copy()
|
|
else:
|
|
return []
|
|
|
|
def __init__(self,
|
|
entropy = None,
|
|
spawn_key: Optional[List[int]] = None,
|
|
pool_size: int = DEFAULT_POOL_SIZE,
|
|
n_children_spawned: int = 0):
|
|
if pool_size < DEFAULT_POOL_SIZE:
|
|
raise ValueError(f"The size of the entropy pool should be at least {DEFAULT_POOL_SIZE}")
|
|
|
|
if entropy is None:
|
|
run_entropy = random_seed_time_pid()
|
|
else:
|
|
run_entropy = _coerce_to_uint32_array(entropy)
|
|
|
|
spawn_entropy = _coerce_to_uint32_array(spawn_key) if spawn_key is not None else empty(0, dtype=u32)
|
|
|
|
# assemble entropy
|
|
if len(spawn_entropy) > 0 and len(run_entropy) < pool_size:
|
|
diff = pool_size - len(run_entropy)
|
|
run_entropy = concatenate((run_entropy, zeros(diff, dtype=u32)))
|
|
entropy_array = concatenate((run_entropy, spawn_entropy))
|
|
|
|
self.entropy = entropy_array # standard NumPy stores original entropy, but much easier to just store array
|
|
self._spawn_key = spawn_key
|
|
self.pool_size = pool_size
|
|
self.n_children_spawned = n_children_spawned
|
|
self.pool = zeros(pool_size, dtype=u32)
|
|
|
|
# mix entropy
|
|
mixer = self.pool
|
|
hash_const = INIT_A
|
|
for i in range(len(mixer)):
|
|
if i < len(entropy_array):
|
|
mixer[i], hash_const = hashmix(entropy_array[i], hash_const)
|
|
else:
|
|
mixer[i], hash_const = hashmix(u32(0), hash_const)
|
|
|
|
for i_src in range(len(mixer)):
|
|
for i_dst in range(len(mixer)):
|
|
if i_src != i_dst:
|
|
value, hash_const = hashmix(mixer[i_src], hash_const)
|
|
mixer[i_dst] = mix(mixer[i_dst], value)
|
|
|
|
for i_src in range(len(mixer), len(entropy_array)):
|
|
for i_dst in range(len(mixer)):
|
|
value, hash_const = hashmix(entropy_array[i_src], hash_const)
|
|
mixer[i_dst] = mix(mixer[i_dst], value)
|
|
|
|
def generate_state(self, n_words: int, dtype: type = u32):
|
|
hash_const = INIT_B
|
|
if dtype is u64:
|
|
n_words *= 2
|
|
elif dtype is not u32:
|
|
compile_error("only support uint32 or uint64")
|
|
|
|
state = zeros(n_words, dtype=u32)
|
|
pool = self.pool
|
|
npool = len(self.pool)
|
|
|
|
for i_dst in range(n_words):
|
|
data_val = pool[i_dst % npool]
|
|
data_val ^= hash_const
|
|
hash_const *= MULT_B
|
|
data_val *= hash_const
|
|
data_val ^= data_val >> XSHIFT
|
|
state[i_dst] = data_val
|
|
|
|
if dtype is u64:
|
|
return ndarray((n_words // 2,), Ptr[u64](state.data.as_byte()))
|
|
else:
|
|
return state
|
|
|
|
def spawn(self, n_children: int):
|
|
seqs = []
|
|
sk: List[int] = self._spawn_key if self._spawn_key is not None else []
|
|
for i in range(self.n_children_spawned,
|
|
self.n_children_spawned + n_children):
|
|
seqs.append(SeedSequence(
|
|
self.entropy,
|
|
spawn_key=(sk + [i]),
|
|
pool_size=self.pool_size
|
|
))
|
|
return seqs
|
|
|