cgol python

This commit is contained in:
Eric Yu 2024-03-02 12:59:24 -08:00
parent 2ce72cee33
commit 29bd4b878d
5 changed files with 537 additions and 0 deletions

11
.gitignore vendored
View File

@ -1 +1,12 @@
__pycache__ __pycache__
out
*/build/*
*/verdiLog/*
*/out/*
*/__pycache__/*
hammer.log
output.json
novas.conf
*.swp
novas.rc
*/python/v/*

180
src/python/cgol.py Normal file
View File

@ -0,0 +1,180 @@
# Not installed by default
# pip3 install --user --upgrade pip
# pip3 install --user Pillow
from PIL import Image
# Not installed by default
# pip3 install --user scipy
from scipy.signal import convolve2d
from tqdm import tqdm # Not installed by default
import numpy as np # Not installed by default
# # Test image generation
# images=[]
# for i in range(0,255):
# arr = np.ones((500, 500, 3), dtype=np.uint8)
# arr[:, :, 1] = i
# images.append(Image.fromarray(arr))
# images_itr = iter(images)
# img = next(images_itr)
# img.save('output_video.gif', format='GIF', append_images=images_itr,
# save_all=True, duration=2, loop=0)
''' CGOL truth table
Neighbors: 2 3 other
Alive A A D
Dead D A D
'''
def cgol_iter(arr: np.ndarray) -> np.ndarray:
# print('next game step...')
X = arr.shape[0]
Y = arr.shape[1]
arr_next = np.zeros((X, Y), dtype=np.uint8)
for x in range(X):
for y in range(Y):
ngh = arr[max(0, x-1):min(X, x+2), max(0, y-1):min(Y, y+2)].sum()
if arr[x,y]:
ngh -= 1
if ngh in (2, 3): arr_next[x, y] = 1
else:
if ngh == 3: arr_next[x, y] = 1
# print('done')
return arr_next
def cgol_iter2(arr: np.ndarray) -> np.ndarray:
X = arr.shape[0]
Y = arr.shape[1]
arr_next = np.zeros((X, Y), dtype=np.uint8)
x_min = max(np.argwhere(arr)[:,0].min()-2, 0)
x_max = min(np.argwhere(arr)[:,0].max()+2, X)
y_min = max(np.argwhere(arr)[:,1].min()-2, 0)
y_max = min(np.argwhere(arr)[:,1].max()+2, Y)
for x in range(x_min, x_max):
for y in range(y_min, y_max):
# 8 single-element reads instead
ngh = 0
if x > 0 and y > 0 : ngh += arr[x-1, y-1] # TL
if y > 0 : ngh += arr[x , y-1] # T
if x < X-1 and y > 0 : ngh += arr[x+1, y-1] # TR
if x > 0 : ngh += arr[x-1, y ] # L
if x < X-1 : ngh += arr[x+1, y ] # R
if x > 0 and y < Y-1: ngh += arr[x-1, y+1] # BL
if y < Y-1: ngh += arr[x , y+1] # B
if x < X-1 and y < Y-1: ngh += arr[x+1, y+1] # BR
if arr[x,y]:
if ngh in (2, 3): arr_next[x, y] = 1 # Stay alive
else:
if ngh == 3: arr_next[x, y] = 1 # Become alive
return arr_next
def cgol_iter3(arr: np.ndarray) -> np.ndarray:
# Perform 'num-neighbors' convolution
ngh_window = np.array([[1,1,1],[1,0,1],[1,1,1]])
ngh_arr = convolve2d(arr, ngh_window, mode='same', boundary='fill', fillvalue=0)
# Generate next CGOL generation with the formula: "arr_next = (3nghs OR (arr AND 2nghs))"
return np.logical_or((ngh_arr==3), np.logical_and(arr, (ngh_arr==2))).astype(np.uint8)
# def board_to_img(arr: np.ndarray, px_size: int) -> Image:
# print('converting to image...')
# X = arr.shape[0]
# Y = arr.shape[1]
# arr_img = np.empty((X*px_size, Y*px_size, 3), dtype=np.uint8)
# for c in (0,1,2): arr_img[:, :, c] = dead_color[c]
# for x in range(X):
# for y in range(Y):
# if arr[x,y]:
# x_start = px_size*x
# y_start = px_size*y
# arr_img[x_start:x_start+px_size,
# y_start:y_start+px_size, :] = alive_color
# print('done')
# return Image.fromarray(arr_img)
def board_to_img2(arr: np.ndarray, px_size: int, a:tuple=(255,255,255), d:tuple=(0,0,0)) -> Image:
# print('converting to image...')
X = arr.shape[0]
Y = arr.shape[1]
arr_img = np.empty((X*px_size, Y*px_size, 3), dtype=np.uint8)
# Repeat matrix to size of board
arr_rep = arr.repeat(px_size, 0).repeat(px_size, 1)
# For each channel, set alive or dead value based on arr_rep
for c in (0,1,2): arr_img[:, :, c] = arr_rep*(a[c]-d[c]) + d[c]
im = Image.fromarray(arr_img)
# print('done')
return im
def save_gif(images: list, fname: str, frame_dur:int=50) -> None:
print('Saving GIF... ', end='', flush=True)
# images_itr = iter(images)
# img = next(images_itr)
# Note: set loop=1 to generate GIFs that play once
images[0].save(fname, format='GIF', append_images=images[1:],
save_all=True, duration=frame_dur, loop=0, optimize=False)
print('done.')
def main():
vid_out_fname = 'cgol.gif'
board_x = 128 #512
board_y = 128 #512
game_length = 800 # 2300
dead_color = (128, 128, 128)
alive_color = ( 0, 255, 0)
grid_px_size = 4
# 'Glider'
# board_init = np.zeros((board_y, board_x), dtype=np.uint8)
# board_init[5,6] = 1
# board_init[6,7] = 1
# board_init[7,5:8] = 1
# # 'Spaceship'
# board_x = 16
# board_y = 256
# game_length = 550
# board_init = np.zeros((board_y, board_x), dtype=np.uint8)
# board_init[2, 7] = 1
# board_init[2, 9] = 1
# board_init[5, 9] = 1
# board_init[3:7, 6] = 1
# board_init[6, 6:9] = 1
# board_init = board_init.transpose()
# 'Acorn' (Goes about 5300)
board_init = np.zeros((board_y, board_x), dtype=np.uint8)
mid = (board_x//2, board_y//2)
board_init[mid[0]-2, mid[1]-1]=1
board_init[mid[0], mid[1]]=1
board_init[mid[0]-3:mid[0]-1, mid[1]+1]=1
board_init[mid[0]+1:mid[0]+4, mid[1]+1]=1
board_init = board_init.transpose()
images = []
board = board_init
print('Generating game...')
for i in tqdm(range(game_length), disable=False):
images.append(board_to_img2(board, grid_px_size, a=alive_color, d=dead_color))
board = cgol_iter3(board)
# Save animated GIF
save_gif(images, vid_out_fname, frame_dur=35)
# Save initial setup of the board
# init_img = board_to_img2(board_init, grid_px_size, a=alive_color, d=dead_color)
images[0].save('cgol_init.gif', format='GIF', save_all=True)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,69 @@
# Configure the Final Project Test Suite
# This needs to match the parameters of the DUT instantiated in the testbench
board:
width: 32
max_length: 1000
display:
# RGB color of 'dead' spaces in the display
dead_color: [128, 128, 128] # grey
# RGB color of 'alive' spaces in the display
alive_color: [ 0, 255, 0] # bright green
# size of the grid in the display
pixel_size: 10
# Durration (ms) of each frame in the display
frame_dur_ms: 50
games: [
# Small and simple oscillator
{
length: 10,
checks: "last", # Only check last frame
init_alive: [
[-1,0], [0,0], [1,0]
],
origin: 'center' # alive coorodinates relative to middle of the board
},
# Common glider
{
length: 50,
checks: "last",
init_alive: [
[2,1], [3,2], [1,3], [2,3], [3,3]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# Common glider (detailed test checking every frame)
{
length: 8,
checks: "all", # Check every frame
init_alive: [
[2,1], [3,2], [1,3], [2,3], [3,3]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# Very large 'Acorn' seed
{
length: 100,
checks: "last",
init_alive: [
[-2,-1], [0,0], [-3,1], [-2,1], [1,1], [2,1], [3,1]
],
origin: "center"
},
# Very large Custom seed
{
length: 300,
checks: "last",
init_alive: [
[2,1], [3,1], [4,1], [-2,1], [-3,1], [-4,1], [2,-1], [3,-1], [4,-1], [-2,-1], [-3,-1], [-4,-1], [6,2], [6,3], [6,4], [6,-2], [6,-3], [6,-4], [-6,2], [-6,3], [-6,4], [-6,-2], [-6,-3], [-6,-4], [2,6], [3,6], [4,6], [-2,6], [-3,6], [-4,6], [2,-6], [3,-6], [4,-6], [-2,-6], [-3,-6], [-4,-6], [1,2], [1,3], [1,4], [-1,-2], [-1,-3], [-1,-4], [-1,2], [-1,3], [-1,4], [1,-2], [1,-3], [1,-4]
],
origin: "center"
},
]

View File

@ -0,0 +1,167 @@
import yaml # Not installed by default
import cgol
import os
import shutil
import numpy as np # Not installed by default
from argparse import ArgumentParser
from tqdm import tqdm # Not installed by default
from math import ceil, log2
# Default values:
trace_data_width = 64 # Data traces should be this many bits wide
output_dir = 'out' # GIFs are written here
default_trace_fout = 'v/bsg_trace_master_0.tr' # Default output file
default_cfg = 'default_sim_cfg.yml' # Default config file
def send_game_req(fout, board:np.ndarray, length, width, max_len):
'''
Given a board and a game length, write this to the trace file so the
data is sent to the DUT.
'''
game_len_formatter = f'{{:0{ ceil(log2(max_len+1)) }b}}'
game_len_bin = game_len_formatter.format(length)
fout.write(f'### sending board config (game length {length}) ###\n')
# Write traces to send data
board_in = board.copy()
board_in = board_in.reshape((width**2, 1))
board_in = board_in.squeeze()
board_in = np.pad(board_in, (0, trace_data_width), 'constant', constant_values=0) # Pad with 0s
wr_idx = 0
while wr_idx < width**2:
if wr_idx==0:
data = np.flip(board_in[0:(trace_data_width-len(game_len_bin))])
wr_idx+=(trace_data_width-len(game_len_bin))
fout.write(f'0001____0_00000000000_{"".join([str(x) for x in data])}_{game_len_bin}\n')
else:
data = np.flip(board_in[wr_idx:wr_idx+trace_data_width])
wr_idx+=trace_data_width
fout.write(f'0001____0_00000000000_{"".join([str(x) for x in data])}\n')
fout.write('\n')
return board
def send_game_check(fout, board:np.ndarray, width):
'''
Given a board, write the output expected trace to the trace file.
'''
fout.write(f'### Checking board output ###\n')
board = board.reshape((width**2, 1))
board = board.squeeze()
board = np.pad(board, (0, trace_data_width), 'constant', constant_values=0) # Pad with 0s
wr_idx = 0
while wr_idx < width**2:
data = np.flip(board[wr_idx:wr_idx+trace_data_width])
wr_idx+=trace_data_width
fout.write(f'0010____0_00000000000_{"".join([str(x) for x in data])}\n')
fout.write('\n')
def add_image_to_list(list, board, disp):
# Add a the next image to a list of images
list.append(cgol.board_to_img2(board, disp['pixel_size'],
a=disp['alive_color'],
d=disp['dead_color']))
def main():
parser = ArgumentParser()
parser.add_argument('-cfg', required=False, default=default_cfg,
help=f'The input config file (in yaml format), defaults to "{default_cfg}"')
parser.add_argument('-out', required=False, default=default_trace_fout,
help=f'Output trace file, defaults to "{default_trace_fout}"')
args = parser.parse_args()
print(f'Clearing any previous output in "{output_dir}"...')
# Create output directory for testbench .data files
try: shutil.rmtree(output_dir)
except(FileNotFoundError): pass
os.mkdir(output_dir)
cfg = yaml.safe_load(open(args.cfg, 'r'))
width = cfg["board"]["width"]
max_len = cfg["board"]["max_length"]
cgol_iterations = 0
cgol_elapsed_time = 0
# split args.out into directory and file name
f = os.path.split(args.out)
# check if the directory exists, if not, create it
if f[0] != '' and not os.path.exists(f[0]):
os.makedirs(f[0])
with open(args.out, 'w') as fout:
# Write trace header
fout.write('')
fout.write(f'# This trace file was generated by "final_project_pre_sim.py" with the config file "{args.cfg}", do not directly modify!\n')
fout.write('\n')
fout.write(f'# Board size: {width}x{width}, max game length: {cfg["board"]["max_length"]}\n')
fout.write(f'# Beginning trace ROM with {len(cfg["games"])} games:\n')
fout.write('\n')
# Write the config for every game...
for (idx, game) in enumerate(cfg['games']):
assert game['length'] <= max_len, 'Max game lenght exceded!'
fout.write(f'########## Game {idx+1} ##########\n')
# Generate the initial board
board = np.zeros((width, width), dtype=np.uint8)
alive_list = game['init_alive']
if game['origin'] == 'center':
for ii, p in enumerate(alive_list): alive_list[ii] = [p[0]+(width//2), p[1]+(width//2)]
for pt in alive_list: board[pt[0]][pt[1]] = 1
board = board.transpose()
expected_fout = f'game_{idx+1}_{width}x{width}_{game["length"]}.gif'
# Generate the trace for this game: only check the last frame
if game["checks"] == "last":
send_game_req(fout, board, game['length'], width, max_len) # Send initial data
print(f'Generating expected result "{expected_fout}" ...')
images = []
# Simulate Game of Life
with tqdm(range(game['length']), disable=False) as tq:
for i in tq:
add_image_to_list(images, board, cfg["display"])
board = cgol.cgol_iter3(board)
# Record stats
cgol_iterations += tq.format_dict['total']
cgol_elapsed_time += tq.format_dict['elapsed']
add_image_to_list(images, board, cfg["display"]) # Add last frame
# Save game as a GIF
cgol.save_gif(images, os.path.join(output_dir, expected_fout), frame_dur=cfg['display']['frame_dur_ms'])
send_game_check(fout, board, width) # Send output check
# Generate the trace for this game: check every frame a long the way
elif game["checks"] == "all":
print(f'Generating expected result "{expected_fout}" ...')
images = []
# Simulate Game of Life
with tqdm(range(game['length']), disable=False) as tq:
for i in tq:
fout.write(f'# Checking frame {i+1}:\n')
send_game_req(fout, board, 1, width, max_len) # Send next frame data (length=1)
add_image_to_list(images, board, cfg["display"])
board = cgol.cgol_iter3(board)
send_game_check(fout, board, width) # Send output check
add_image_to_list(images, board, cfg["display"]) # Add last frame
# Save output image
cgol.save_gif(images, os.path.join(output_dir, expected_fout), frame_dur=cfg['display']['frame_dur_ms'])
# Invalid option
else:
assert False, f'ERROR: invalid "checks" option: {game["checks"]}'
# After all games finished...
fout.write(f'########## SIMULATION FINISHED ##########\n')
fout.write(f'0011____0_00000000000_{64*"0"}\n')
# After output file closed...
print('Done.')
print()
# Print performancs stats
print('CGoL script performance: (measured with "checks=last" games only)')
print(f' Computed and saved a total of {cgol_iterations} CGoL frames of size {width}x{width} over {cgol_elapsed_time:.3f} seconds')
if cgol_elapsed_time==0 : cgol_elapsed_time = float('nan') # Handle case where there were no 'last' games
print(f' Average performance {cgol_iterations/cgol_elapsed_time:.3f} frames/second')
print()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,110 @@
# Configure the Final Project Test Suite
# This needs to match the parameters of the DUT instantiated in the testbench
board:
width: 3
max_length: 1
display:
# RGB color of 'dead' spaces in the display
dead_color: [128, 128, 128] # grey
# RGB color of 'alive' spaces in the display
alive_color: [ 0, 255, 0] # bright green
# size of the grid in the display
pixel_size: 20
# Durration (ms) of each frame in the display
frame_dur_ms: 1000
games: [
# No neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# 1 neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1], [0,0]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# 2 neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1], [0,0], [0, 1]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# 3 neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1], [0,0], [0, 1], [0, 2]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# 4 neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1], [0,0], [0, 1], [0, 2], [1, 2]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# 5 neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1], [0,0], [0, 1], [0, 2], [1, 2], [2, 2]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# 6 neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1], [0,0], [0, 1], [0, 2], [1, 2], [2, 2], [2, 1]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# 7 neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1], [0,0], [0, 1], [0, 2], [1, 2], [2, 2], [2, 1], [2, 0]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
# 8 neighbors
{
length: 1,
checks: "last",
init_alive: [
[1,1], [0,0], [0, 1], [0, 2], [1, 2], [2, 2], [2, 1], [2, 0], [1, 0]
],
origin: "corner" # alive coorodinates relative to upper-left corner of the board
},
]