Strong suspicions that the game of life might not be a good base for a PRNG : Takens killed my hopes

Previously, I was having high hopes of doing open science on PRNG (Pseudo Random Number Generator) based on the game of life on an hexagonal board. However, today, after a little bit of investigations I have bad and good news on the topic.

The bad news



Takens series might have killed my hopes, but it did it before I invested much time



Takens' series are a useful tool for evaluating the quality of randomness. When choosing this method I did know what to reach as a goal, I had a compass, and this compass was filling the space phase uniformely without patterns : f(x) => f(x+1) should not show signs of preference for space. Seeing my tweaked game of life with enhanced self injected entropy converge to a Sierpinsky gasket. Maybe it's not totally bad given I don't know it's lyapounov exponent (aka fractal dimension), but I refuse to let anyone use a method that displays strongly biased entropy in the space phase.

Good randomness for scientific usage or cryptographic one should be evenly distributed. Here is a pertubed game of life, with self injection of « chaos » into itself displaying a Sierpinky Gasket as a strange attractor, followed by a test sample of consecutive deterministric pseudo random numbers which source is the random python's PRNG.



Normally, I can conclude here with : « don't let friends use biased randomness and don't roll your own crypto ».

But I won't : first and foremost I want to higlight that keeping a clear view of what you do is important and knowing fast when you fail is as much postive experience as succeeding slowly.



The good news



The importance of visualisation



Since the beginning of the experiment as a python-tk proof of concept I added a visual interface. Here is the interface :


I added the possibility not only to see in real time the Takens series, but also the non overlapping evolutions of the pertubed game of life with the original one (level of gray coding the discrepancies).

I can testify I detected A LOT OF bugs through this. Manipulating code is easy, and making mistakes even easier. By keeping a view of the initial problem I kept a strong way of checking everything was making sense.

Data visualisation is the second compass with Takens series that helped me save a lot of time to not conclude falsely I made a unique discovery.

python-tk is bearing the load



Every cycle, I communicate back and forth between tk and python, python sending up to 2500 tcl commands in less than 1 second to update de canvas. Just for this it proved useful in giving me trust in the concept.

I was often asked why not tkinter or pure tcl ?

Well, python has data structures such as sets that supports operations that maps nicely with the concepts used in bit arithmetic (xor, and, or ...) that tcl does not have.
In python I can also additionnaly call matplotlib if I want to do 3D scatters or use the very performant numpy arrays. On the other hand, I have experience in Tk/Tcl and have a better grasp of how I deal with GUI in Tk rather than in tkinter. It's a question of comfort not a question of « one best way ». And it was fun to code this way without to much abstractions in the way.

Is the experiment over ?



Yes, it is time consuming.

However, I strongly suspect and would like to check in the future, that the strange attractor in the shape of a Sierpinsky Gasket is due to the symetry in the first neighbours in an hexagonal shape. I would -if I was paid for it explore other kinds of neighbouroud and rules.

For instance we could add a second ring of neighbours and maybe use them as anti-ferromagnetic rules. Maybe I would use random neighbourhood to check if the Sierpinsky gasket disappears .... and so long.

The nice part for this experimentations would be that I have the tooling that is ready.

Annexe the code



The code is available here as a gist on github
from time import time, sleep
import select
from subprocess import Popen, PIPE
from random import randint
import random
from os import system
import os
ORD=0
SZ=12
DIMX=64
DIMY=64
SSP=50
old_alivep=set()
new_alivep=set()
old_alive=set()
new_alive=set()
dead_alive_percent=50
pause=False
print_board=True
set_alive={ 3, 4, 5}
set_dead={ 3, 4, 5}
old_old_samplep=0
old_samplep=0
samplep = 0
old_old_sample=0
old_sample=0
sample = 0
sample_mask=set([])
plt_pause = False
system("rm *ps output2.mp4 ev*.jpg")
seed=int(time())
print("seed %d" % seed)
random.seed(seed)
save_canvas = True
ORD=1
# let's talk to tk/tcl directly
p = Popen(['wish'], stdin=PIPE, stdout=PIPE, stderr=PIPE)
os.set_blocking(p.stdout.fileno(), False)
os.set_blocking(p.stdin.fileno(), False)
os.set_blocking(p.stderr.fileno(), False)
n=0
def puts(s):
global n
n+=1
for l in s.split("\n"):
select.select([], [p.stdin],[])
p.stdin.write((l + "\n").encode())
if back := p.stderr.read():
print("TCL>" + back.decode())
def gets():
ret=p.stdout.read()
if ret:
print(ret)
exec(ret, globals(), )
puts(f"""
package require Tk
set code [ catch {{
source /usr/share/tcltk/ttkthemes/themes/plastik/plastik.tcl
ttk::style theme use plastik
}} pass ]
set status 0
proc SaveAsC {{}} {{
set out [tk_getSaveFile]
set code [ catch {{.c postscript -file $out }} result ]
if {{$result != ""}} {{
error $result
}}
}}
proc SaveAsC2 {{}} {{
set out [tk_getSaveFile]
set code [ catch {{.c2 postscript -file $out }} result ]
if {{$result != ""}} {{
error $result
}}
}}
proc SaveAsC3 {{}} {{
set out [tk_getSaveFile]
set code [ catch {{.c3 postscript -file $out }} result ]
if {{$result != ""}} {{
error $result
}}
}}
proc SaveAsC4 {{}} {{
set out [tk_getSaveFile]
set code [ catch {{.c4 postscript -file $out }} result ]
if {{$result != ""}} {{
error $result
}}
}}
if {{$code}} {{
puts "print('theme not found, fallback to ugly tcl/tk look')"
puts "print('on debian : apt install tcl-ttkthemes to have ttkthemes installed')"
}}
pack [ ttk::frame .t ]
pack [ ttk::frame .tt1 ] -in .t -anchor s -side left
pack [ ttk::frame .tt2 ] -in .t -anchor s -side left
pack [ ttk::frame .tt3 ] -in .t -anchor s -side left
pack [ ttk::label .ll2 -text "board of the game of life" ] -in .tt1
pack [ canvas .c -width {DIMX*SZ*1.01} -height { DIMY*SZ*.87 } -bg white ] -in .tt1 -anchor s
pack [ttk::button .bc -text "Save as postscript" -command SaveAsC ] -in .tt1 -anchor s
pack [ ttk::label .ll1 -text "x,y = u(n-1),u(n) aka Takens Serie " ] -in .tt2 -anchor s
pack [ canvas .c2 -width {DIMX*SZ*1.01//2} -height { DIMY*SZ*.87//2 } -bg white ] -in .tt2 -anchor s
.c configure -bg white
pack [ttk::button .bc2 -text "Save as postscript" -command SaveAsC2 ] -in .tt2 -anchor s
pack [ ttk::label .ll3 -text "Takens Serie for perturbed serie" ] -in .tt2 -anchor s
pack [ canvas .c3 -width {DIMX*SZ//2*1.01} -height { DIMY*SZ*.87//2 } -bg white ] -in .tt2 -anchor s
pack [ttk::button .bc3 -text "Save as postscript" -command SaveAsC3 ] -in .tt2 -anchor s
pack [ ttk::label .ll4 -text "Takens Serie for random" ] -in .tt3 -anchor s
pack [ canvas .c4 -width {DIMX*SZ//2*1.01} -height { DIMY*SZ*.87//2 } -bg white ] -in .tt3 -anchor s
pack [ttk::button .bc4 -text "Save as postscript" -command SaveAsC4 ] -in .tt3 -anchor s
pack [ ttk::frame .first ] -expand true -fill both -anchor s
pack [ ttk::label .s -textvariable status ] -anchor s -in .first
pack [ ttk::frame .f ] -anchor s -expand true -fill both
pack [ ttk::frame .center ] -anchor s -in .f
ttk::button .r -text Reset -command {{ puts seed() }}
ttk::button .b -text Pause -command {{ puts pause^=True }}
ttk::button .quit -text Quit -command {{ destroy . }}
pack .r .b .quit -in .center -anchor se -side left
pack [ ttk::labelframe .rules -text "rules change" ] -in .f -expand true -fill both -padx 5 -pady 5
pack [ ttk::frame .g ] -anchor s -expand true -fill both -in .rules -padx 5
pack [ ttk::checkbutton .v1 -text 1 -command {{ puts "seed();set_alive^=set({1,})" }}] -in .g -anchor w -side left
pack [ ttk::label .s1 -text " " ] -in .g -anchor w -side left
pack [ ttk::checkbutton .v2 -text 2 -command {{ puts "seed();set_alive^=set({2,})" }}] -in .g -anchor w -side left
pack [ ttk::label .s2 -text " " ] -in .g -anchor w -side left
pack [ ttk::checkbutton .v3 -text 3 -onvalue 1 -variable a -command {{ puts "seed();set_alive^=set({3,})" }}] -in .g -anchor w -side left
set a 1
set b 1
set c 1
set d 1
set e 1
set f 1
pack [ ttk::label .s3 -text " " ] -in .g -anchor w -side left
pack [ ttk::checkbutton .v4 -text 4 -onvalue 1 -variable b -command {{ puts "seed();set_alive^=set({4,})" }}] -in .g -anchor w -side left
pack [ ttk::label .s4 -text " " ] -in .g -anchor w -side left
pack [ ttk::checkbutton .v5 -text 5 -onvalue 1 -variable c -command {{ puts "seed();set_alive^=set({5,})" }}] -in .g -anchor w -side left
pack [ ttk::label .s5 -text " " ] -in .g -anchor w -side left
pack [ ttk::checkbutton .v6 -text 6 -command {{ puts "seed();set_alive^=set({6,})" }}] -in .g -anchor w -side left
pack [ ttk::label .s6 -text " " ] -in .g -anchor w -side left
pack [ ttk::label .v -text "neighbour alive to give life" ] -in .g -anchor w -side left
pack [ ttk::frame .h ] -anchor s -expand true -fill both -in .rules -padx 5
pack [ ttk::checkbutton .d1 -text 1 -command {{ puts "seed();set_dead^=set({1,})" }}] -in .h -anchor w -side left
pack [ ttk::label .t1 -text " " ] -in .h -anchor w -side left
pack [ ttk::checkbutton .d2 -text 2 -command {{ puts "seed();set_dead^=set({2,})" }}] -in .h -anchor w -side left
pack [ ttk::label .t2 -text " " ] -in .h -anchor w -side left
pack [ ttk::checkbutton .d3 -text 3 -onvalue 1 -variable d -command {{ puts "seed();set_dead^=set({3,})" }}] -in .h -anchor w -side left
pack [ ttk::label .t3 -text " " ] -in .h -anchor w -side left
pack [ ttk::checkbutton .d4 -text 4 -onvalue 1 -variable e -command {{ puts "seed();set_dead^=set({4,})" }}] -in .h -anchor w -side left
pack [ ttk::label .t4 -text " " ] -in .h -anchor w -side left
pack [ ttk::checkbutton .d5 -text 5 -onvalue 1 -variable f -command {{ puts "seed();set_dead^=set({5,})" }}] -in .h -anchor w -side left
pack [ ttk::label .t5 -text " " ] -in .h -anchor w -side left
pack [ ttk::checkbutton .d6 -text 6 -command {{ puts "seed();set_dead^=set({6,})" }}] -in .h -anchor w -side left
pack [ ttk::label .t6 -text " " ] -in .h -anchor w -side left
pack [ ttk::label .d -text "neighbour dead to give life" ] -in .h -anchor w -side left
pack [ ttk::frame .l ] -expand true -fill both -in .rules -padx 5 -pady 5
set seed 50
pack [ ttk::spinbox .sc -width 4 -from 0 -to 100 -textvariable seed -command {{
puts "dead_alive_percent=$seed;seed()"
}}
] -in .l -anchor w -side left
pack [ ttk::label .scl -text " re seed session with a different percentage of dead/alive ratio" ] -in .l -anchor w -side left
pack [ ttk::frame .k ] -expand true -fill both
pack [ ttk::checkbutton .tb -text "disable printing of board" -command "puts {{print_board^=True}}" ] -in .k -anchor w -side left -padx 5 -pady 5
#.tb invoke
pack [ ttk::frame .kk ] -expand true -fill both
pack [ ttk::checkbutton .tcv -text "disable saving of canvas" -command "puts {{save_canvas^=True}}" ] -in .kk -anchor w -side left -padx 5 -pady 5
.tcv invoke
""")
def save(name="save"):
global ORD, save_canvas
ORD+=1
puts(f"set status {ORD}")
puts(f""".c postscript -file {name}-{"%04d"% ORD}.ps""")
puts(f""".c2 postscript -file t{name}-{"%04d"% ORD}.ps""")
def cell(x,y, **kw):
hs=0.866 # heigth of an equilateral triangle
px,py=x/2-1/2*(y%2),y-2/3
#px,py=x-(y%2),y-2/3
xr,yr=px*SZ+SZ,py*hs*SZ+SZ
# triangle compliqué à dessiner
r=SZ/2
#from pdb import set_trace; set_trace()
puts(f"""
.c create oval {xr-r} {yr-r} {xr+r} {yr+r} -width 0 -outline "" {["","-fill black "][kw.get("state","dead")=="alive"]} -tags [ list "p:{x//2},{y}" ]\n""")
def get_neighbour(x,y):
ye = [(-1, -1), (1, 0), (0, -1), (-1, 1), (-1,0), (0, 1)]
yo= [(1, 1), (-1, 0), (0, -1), (1, 0), (0, 1), (1, -1) ]
return set( ((dxdy[0]+x)%DIMX,(y+dxdy[1])%DIMY) for dxdy in [yo,ye][y%2])
def clear():
global ORD
ORD=0
system("rm *ps output2.mp4 ev*.jpg")
puts(".c delete all")
puts(".c2 delete all")
puts(".c3 delete all")
puts(".c4 delete all")
pick_sample()
def pick_entropy(board, _from, _to,w=0):
entropy=0
for o,i in enumerate(range(_from, _from+_to*2,2)):
if (w,i) in board:
entropy |= 1<<o
return entropy
def flipbit(board,w):
for i in range(0,10):
board ^= {(pick_entropy(board,i*6,6,w), pick_entropy(board,(i+1)*6,6,w))}
def seed():
global old_alive, dead_alive_percent, old_alivep
clear()
for x in range(0,DIMX*2,2):
for y in range(DIMY):
state = randint(0, 100) >= dead_alive_percent and "alive" or "dead"
if state=="alive":
old_alive|= {(x/2,y),}
cell(
x,
y,
state=state,
outline="black")
old_alivep = old_alive.copy()
for i in range(64):
flipbit(old_alivep,i)
def live_neighbour(x,y,board):
return sum(map(lambda x : x in board,get_neighbour(x,y)))
def pick_sample():
global sample_mask
sample_mask=set([])
for i in range(SSP):
x, y=(randint(0, DIMX),randint(0, DIMY))
sample_mask |= set([( x, y, )])
def plot_sample():
global old_sample, old_old_sample, old_old_samplep, sample_mask, new_alive, ORD, SZ, sample, sample_array, plt_pause, samplep, old_samplep, new_alivep, old_alivep
old_old_sample=old_sample
old_sample = sample
sample = 0
old_old_samplep=old_samplep
old_samplep = samplep
samplep = 0
for i, coord in enumerate(sample_mask):
if coord in new_alive:
sample |= 1<<i
if coord in new_alivep:
samplep |= 1<<i
_max = ((1<<(len(sample_mask))))
r=_max//512
if ORD > 1:
_max = ((1<<(len(sample_mask))) // SZ*2)
cr=.8
x = old_sample
y = sample
puts(f""".c2 create oval {(x -r)*DIMX*cr//_max} {(y -r )*DIMY*cr//_max} {(x+r)*DIMX*cr//_max} {(y+r)*DIMY*cr//_max}""")
x = old_samplep
y = samplep
puts(f""".c3 create oval {(x -r)*DIMX*cr//_max} {(y -r )*DIMY*cr//_max} {(x+r)*DIMX*cr//_max} {(y+r)*DIMY*cr//_max} """)
random.seed(ORD)
x=randint(0,_max*6)
y=randint(0,_max*6)
random.seed(time())
puts(f""".c4 create oval {(x -r)*DIMX*cr//_max} {(y -r )*DIMY*cr//_max} {(x+r)*DIMX*cr//_max} {(y+r)*DIMY*cr//_max} """)
def will_live(x,y, old_board, new_board):
global set_dead, set_alive
ostate = (x,y) not in old_board and 'dead' or "alive"
lively = live_neighbour(x,y, old_board)
#print("%d-%d:%s:%d" % (x,y,ostate,lively))
state = ( ostate == "alive" and lively in set_alive ) or (
ostate == "dead" and lively in set_dead )
if(state):
new_board |= {(x,y),}
return state
seed()
save("ev")
while True:
gets()
sleep(0.0001)
while pause:
sleep(.1)
gets()
for x in range(DIMX):
for y in range(DIMY):
will_live(x,y, old_alivep, new_alivep)
will_live(x,y, old_alive, new_alive)
print_board and puts(f""".c itemconfigure "p:{x},{y}" -fill {["white","lightgray", "darkgray", "black"][((x,y) in new_alive) + 2* ((x,y) in new_alivep)]}""")
flipbit(new_alivep,0)
flipbit(new_alivep,1)
plot_sample()
puts("update")
old_alive=new_alive
old_alivep=new_alivep
new_alive=set()
new_alivep=set()
save("ev")
view raw gof_pertubed.py hosted with ❤ by GitHub

No comments: