diff --git a/.gitignore b/.gitignore index bee8a64..c4a2d70 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,12 @@ __pycache__ +out +*/build/* +*/verdiLog/* +*/out/* +*/__pycache__/* +hammer.log +output.json +novas.conf +*.swp +novas.rc +*/python/v/* diff --git a/src/python/cgol.py b/src/python/cgol.py new file mode 100644 index 0000000..6e99085 --- /dev/null +++ b/src/python/cgol.py @@ -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() diff --git a/src/python/default_sim_cfg.yml b/src/python/default_sim_cfg.yml new file mode 100644 index 0000000..e745d34 --- /dev/null +++ b/src/python/default_sim_cfg.yml @@ -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" + }, +] diff --git a/src/python/final_proj_pre_sim.py b/src/python/final_proj_pre_sim.py new file mode 100644 index 0000000..4f8fba0 --- /dev/null +++ b/src/python/final_proj_pre_sim.py @@ -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() diff --git a/src/python/small_sim_cfg.yml b/src/python/small_sim_cfg.yml new file mode 100644 index 0000000..ccdf1b5 --- /dev/null +++ b/src/python/small_sim_cfg.yml @@ -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 + }, + +]