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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
No comments:
Post a Comment