Source code for Circuit.FileBasedCircuit

  1"""ICE40 ASC-file circuit implementation.
  2
  3Provides :class:`FileBasedCircuit`, which reads and writes bitstreams in the
  4IceStorm ASCII (``.asc``) format, compiles them via ``icepack``, and uploads
  5to hardware via ``iceprog``. Uses memory-mapped file I/O for performance.
  6
  7.. note::
  8    Magic tile coordinates and routing types are specific to the ICE40 HX1K.
  9    Different FPGA models will require different constants.
 10"""
 11
 12from mmap import mmap
 13from pathlib import Path
 14from shutil import copyfile
 15from subprocess import run
 16import os
 17from time import sleep
 18from BitstreamEvolutionProtocols import FPGA_Compilation_Data
 19from Circuit.Circuit import Circuit
 20from Directories import Directories
 21from Logger import Logger
 22from returns.result import Result, Success, Failure # type: ignore
 23
 24COMPILE_CMD = "icepack"
 25RUN_CMD = "iceprog"
 26
[docs] 27class FileBasedCircuit(Circuit): 28 """ 29 Represents a Circuit that is based on an ASC file, in a format 30 ready to be compiled by IceStorm tools. 31 Provides useful methods for working with hardware files 32 """ 33 34 def __init__(self, *, index: int, filename: str, template: Path, logger: Logger, directories: Directories, 35 routing_type: str, accessed_columns: list[int]): 36 Circuit.__init__(self, index, filename, logger) 37 self.__routing_type = routing_type 38 self.__accessed_columns = accessed_columns 39 40 asc_dir = directories.asc_dir 41 bin_dir = directories.bin_dir 42 data_dir = directories.data_dir 43 self.__hardware_filepath = asc_dir.joinpath(filename + ".asc") 44 self.__bitstream_filepath = bin_dir.joinpath(filename + ".bin") 45 46 # Create directories if they don't exist 47 os.makedirs(asc_dir, exist_ok=True) 48 os.makedirs(bin_dir, exist_ok=True) 49 os.makedirs(data_dir, exist_ok=True) 50 51 # NOTE Using log files instead of a data buffer in the event of premature termination 52 self._data_filepath = data_dir.joinpath(filename + ".log") 53 # Create the data file if it doesn't exist 54 open(self._data_filepath, "w+").close() 55 56 if template: 57 copyfile(template, self.__hardware_filepath) 58 59 # Since the hardware file is written to and read from a lot, we 60 # mmap it to improve preformance. 61 hardware_file = open(self.__hardware_filepath, "r+") 62 self._hardware_file = mmap(hardware_file.fileno(), 0) 63 hardware_file.close() 64
[docs] 65 def copy_from(self, other: 'FileBasedCircuit'): 66 # Close our mmap so the file can be overwritten (required on Windows) 67 self._hardware_file.close() 68 copyfile(other.__hardware_filepath, self.__hardware_filepath) 69 # Re-open and re-mmap 70 hardware_file = open(self.__hardware_filepath, "r+") 71 self._hardware_file = mmap(hardware_file.fileno(), 0) 72 hardware_file.close()
73
[docs] 74 def compile(self, fpga: FPGA_Compilation_Data) -> Result[None,Exception]: 75 """ 76 Compiles and uploads the compiled circuit and runs it on the FPGA 77 """ 78 79 self.__compile() 80 81 cmd_str = [ 82 RUN_CMD, 83 self.__bitstream_filepath, 84 "-d", 85 fpga.id 86 ] 87 print(cmd_str) 88 run(cmd_str) 89 sleep(1) 90 91 return Success(None)
92 93 # if switching fpgas every sample, need to upload to the second fpga also 94 # if self._config.get_transfer_sample(): 95 # cmd_str = [ 96 # RUN_CMD, 97 # self._bitstream_filepath, 98 # "-d", 99 # self._config.get_fpga2() 100 # ] 101 # print(cmd_str) 102 # run(cmd_str) 103 # sleep(1) 104 105 def __run_at_each_modifiable(self, lambda_func, hardware_file = None, accessible_columns = None, 106 routing_type=None): 107 """ 108 Runs the lambda_func at every modifiable position 109 Args passed to lambda_func: value of bit (as a byte), row, col 110 If lambda_func returns a value (byte), then the bit is set to that number 111 If lambda_func returns None, then the bit is left unmodified 112 Keep in mind the bytes are the ASCII codes, so for example 49 = 1 113 114 .. warning:: 115 Go over this with someone who can clarify what all of the data types are. 116 117 Parameters 118 ---------- 119 lambda_func : Callable 120 This function is called for each modifiable bit. The bit value is passed into lambda_func. 121 If lambda_func returns None, the bit is left unmodified. If lambda_func returns another value, 122 then the bit at that position is replaced with the return value of lambda_func. 123 hardware_file : str | None 124 The path to the hardware file to read from/write to. If no value provided, uses this Circuit's hardware file. 125 accessible_columns : list[str] | None 126 The accessible columns. If no value provided, uses the current configuration value. 127 routing_type: str | None 128 The routing type (MOORE or NEWSE). If no value provided, uses the current configuration value. 129 """ 130 131 if hardware_file is None: 132 hardware_file = self._hardware_file 133 134 if accessible_columns is None: 135 accessible_columns = self.__accessed_columns 136 137 if routing_type is None: 138 routing_type = self.__routing_type 139 140 # Set tile to the first location of the substring ".logic_tile" 141 # The b prefix makes the string an instance of the "bytes" type 142 # The .logic_tile header indicates that there is a tile, so the "tile" variable stores the starting point of the current tile 143 tile = hardware_file.find(b".logic_tile") 144 145 while tile > 0: 146 # Set pos to the position of this tile, but with the length of ".logic_tile" added so it is in front of where we have the x/y coords 147 pos = tile + len(".logic_tile") 148 149 # Check if the position is legal to modify 150 if self.__tile_is_included(hardware_file, pos): 151 # Find the start and end of the line; the positions of the \n newline just before and at the end of this line 152 # The start is the newline position + 1, so the first valid bit character 153 # line_size is self-explanatory 154 # This finds the length of a standard line of bits (so the width of each data-containing line in this tile) 155 line_start = hardware_file.find(b"\n", tile) + 1 156 line_end = hardware_file.find(b"\n", line_start + 1) 157 line_size = line_end - line_start + 1 158 159 # Determine which rows we can modify 160 # TODO ALIFE2021 The routing protocol here is dated and needs to mimic that of the Tone Discriminator 161 rows = [] 162 if routing_type == "MOORE": 163 rows = [1, 2, 13] 164 elif routing_type == "NEWSE": 165 rows = [1, 2] 166 # Iterate over each row and the columns that we can access within each row 167 for row in rows: 168 for col in accessible_columns: 169 # This will get us to individual bits. If the mutation probability passes 170 # Our position is now going to be the start of the first line, plus the line size multiplied to get to our desired row, 171 # and finally added to the column (with the int cast to sanitize user input) 172 pos = line_start + line_size * (row - 1) + int(col) 173 bit_value = hardware_file[pos] 174 lambda_return = lambda_func(bit_value, row, col) 175 if lambda_return is not None: 176 # need to re-assign the bit 177 hardware_file[pos] = lambda_return 178 179 # Find the next logic tile, and start again 180 # Will return -1 if .logic_tile isn't found, and the while loop will exit 181 tile = hardware_file.find(b".logic_tile", tile + 1) 182 183 def __compile(self): 184 """ 185 Compile circuit ASC file to a BIN file for hardware upload. 186 """ 187 self.__log_event(2, "Compiling", self, "with icepack...") 188 189 # Ensure the file backing the mmap is up to date with the latest 190 # changes to the mmap. 191 self._hardware_file.flush() 192 193 compile_command = [ 194 COMPILE_CMD, 195 self.__hardware_filepath, 196 self.__bitstream_filepath 197 ] 198 run(compile_command) 199 200 self.__log_event(2, "Finished compiling", self) 201 202 def __tile_is_included(self, hardware_file, pos): 203 """ 204 Determines whether a given tile is available for modificiation. 205 NOTE: Tile = the .logic_tile in the asc file. 206 207 .. warning:: 208 Preexisting todo: Replace magic values with a more generalized solution. 209 These magic values are indicative of the underlying hardware (ice40kh1k) 210 211 Parameters 212 ---------- 213 hardware_file : mmap 214 Memory Mapped hardware file 215 pos : int 216 Index of the first byte in the .asc file for the hardware 217 218 Returns 219 ------- 220 bool 221 True if the tile at that position is valid (The Tiles we can modigy) 222 """ 223 # Replace these magic values with a more generalized solution 224 # Magic values are indicative of the underlying hardware (ice40hx1k) 225 # A different model will require different magic values (i.e. ice40hx8k) 226 VALID_TILE_X = range(4, 10) 227 VALID_TILE_Y = range(1, 17) 228 229 # NOTE x and y are stored as ints to aid the loops that search and identify 230 # tiles while scraping the asc files 231 # This is in the actual asc file; this is why we can simply pull from "pos" 232 # i.e. you'll see the header ".logic_file 1 1" - x=1, y=1 233 234 # This is where we had a fundamental issue before: The value in the hardware at this position is going to be an ASCII char value, not the actual 235 # number. Here, we parse the byte as an integer, then to a char, then back to an integer 236 237 # However, we have a great problem now: what about multi-digit numbers? 238 # Find the space that separates the x and y, and find the end of the line 239 # Then, grab the bytes for x, grab the bytes for y, convert to strings, and parse those strings 240 space_pos = hardware_file.find(b" ", pos + 1) 241 eol_pos = hardware_file.find(b"\n", pos) 242 x_bytes = hardware_file[pos:space_pos] 243 y_bytes = hardware_file[space_pos:eol_pos] 244 x_str = x_bytes.decode("utf-8").strip() 245 y_str = y_bytes.decode("utf-8").strip() 246 x = int(x_str) 247 y = int(y_str) 248 is_x_valid = x in VALID_TILE_X 249 is_y_valid = y in VALID_TILE_Y 250 251 return is_x_valid and is_y_valid 252
[docs] 253 def get_bitstream(self) -> list[bool]: 254 bitstream: list[bool] = [] 255 def add_bit(bit: int, *rest): 256 if bit == 48: 257 bitstream.append(False) 258 elif bit == 49: 259 bitstream.append(True) 260 self.__run_at_each_modifiable(add_bit) 261 return bitstream
262
[docs] 263 def set_bitstream(self, bitstream: list[bool]): 264 index = 0 265 def get_bit(*rest): 266 nonlocal index 267 bit = bitstream[index] 268 index = index + 1 269 if bit: 270 return 49 271 else: 272 return 48 273 self.__run_at_each_modifiable(get_bit)
274
[docs] 275 @staticmethod 276 def get_file_attribute_st(mmapped_file, attribute): 277 ''' 278 Returns the value of the stored attribute from the hardware file. 279 Circuits are capable of storing string name-value pairs in their hardware file, for purposes such as 280 tracking most recently-evaluated fitness of a Circuit 281 Static version of get_file_attribute that requires the memory-mapped file to be provided 282 283 Parameters 284 ---------- 285 mmapped_file : mmap 286 The memory-mapped hardware file of the circuit 287 attribute : str 288 Attribute name to lookup 289 290 Returns 291 ------- 292 str 293 File attribute value 294 ''' 295 index = mmapped_file.find(b".comment FILE_ATTRIBUTES") 296 if index < 0: 297 return '0' 298 else: 299 newline_index = mmapped_file.find(b'\n', index) 300 searchable_area = mmapped_file[index:newline_index] 301 attr_index = searchable_area.find(bytes(attribute + '={', 'utf-8')) 302 if attr_index < 0: # Value doesn't exist yet 303 return '0' 304 attr_index = attr_index + len(attribute + "={") 305 end_index = searchable_area.find(b'}', attr_index) 306 value_bytes = searchable_area[attr_index:end_index] 307 return str(value_bytes, 'utf-8')
308
[docs] 309 @staticmethod 310 def set_file_attribute_st(hardware_file, attribute, value): 311 ''' 312 Sets a Circuit's file attribute to the specified value 313 Circuits are capable of storing string name-value pairs in their hardware file, for purposes such as 314 tracking most recently-evaluated fitness of a Circuit 315 Static version of set_file_attribute that requires the memory-mapped file to be provided 316 317 Parameters 318 ---------- 319 hardware_file : mmap 320 The memory-mapped hardware file of the Circuit 321 attribute : str 322 The name of the attribute to modify 323 value : str 324 The value to assign to the attribute 325 ''' 326 # Check if the comment exists 327 #hardware_file = open(file_path, "r+") 328 mmapped_file = mmap(hardware_file.fileno(), 0) 329 index = mmapped_file.find(b".comment FILE_ATTRIBUTES") 330 if index < 0: 331 # Create the comment 332 comment_line = ".comment FILE_ATTRIBUTES " + attribute + "={" + value + "}\n" 333 # This requires re-mapping the self._hardware_file 334 #hardware_file = open(file_path, "r+") 335 content = hardware_file.read() 336 hardware_file.seek(0, 0) 337 hardware_file.write(comment_line + content) 338 else: 339 # Check if the attribute exists 340 end_index = mmapped_file.find(b'\n', index) 341 line = str(mmapped_file[index:end_index], 'utf-8') 342 attr_index = line.find(attribute + "={") 343 lines = hardware_file.readlines() 344 line_index = 0 # Index of the line that contains the attribute comment 345 for l in lines: 346 if l.find(".comment FILE_ATTRIBUTES") >= 0: 347 break 348 line_index = line_index + 1 349 350 if attr_index < 0: 351 # Attribute doesn't exist yet 352 line = line + " " + attribute + "={" + value + "}\n" 353 else: 354 attr_end_index = line.find('}', attr_index) + 2 355 before_attr = line[:attr_index] 356 after_attr = line[attr_end_index:] 357 line = before_attr + attribute + "={" + value + "} " + after_attr + '\n' 358 lines[line_index] = line 359 hardware_file.truncate(0) 360 hardware_file.seek(0) 361 hardware_file.writelines(lines)
362
[docs] 363 def get_file_attribute(self, attribute) -> str | None: 364 ''' 365 Returns the value of the stored attribute for this Circuit 366 Circuits are capable of storing string name-value pairs in their hardware file, for purposes such as 367 tracking most recently-evaluated fitness of a Circuit 368 369 Parameters 370 ---------- 371 attrbute : str 372 The name of the attribute of this circuit you want 373 374 Returns 375 ------- 376 str 377 The value of the attribute 378 ''' 379 return FileBasedCircuit.get_file_attribute_st(self._hardware_file, attribute)
380
[docs] 381 def set_file_attribute(self, attribute, value): 382 ''' 383 Sets this Circuit's file attribute to the specified value 384 Circuits are capable of storing string name-value pairs in their hardware file, for purposes such as 385 tracking most recently-evaluated fitness of a Circuit 386 387 Parameters 388 ---------- 389 attribute : str 390 The name of the attribute to modify 391 value : str 392 The value to assign to the attribute 393 ''' 394 hardware_file = open(self.__hardware_filepath, "r+") 395 FileBasedCircuit.set_file_attribute_st(hardware_file, attribute, value) 396 # Re-map our hardware file 397 self._hardware_file = mmap(hardware_file.fileno(), 0) 398 hardware_file.close()
399 400 def __log_event(self, level, *event): 401 """ 402 Emit an event-level log. This function is fulfilled through 403 the logger. 404 """ 405 self._logger.log_event(level, *event)